Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ DATABASE_URL="YOUR_NOT_SO_SECRET_DATABASE_URL_GOES_HERE"
LEMON_SQUEEZY_API_KEY="" # Just get it from somewhere
LEMON_SQUEEZY_STORE_ID="" # Just get it from somewhere
LEMON_SQUEEZY_VARIANT_ID="" # Just get it from somewhere
LEMON_SQUEEZY_WEBHOOK_SECRET=
TEST_API_KEY=
REDIS_URL=
6 changes: 2 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

## Commands

- **Test all**: `bun test`
- **Test single file**: `vitest src/__tests__/unit/path/to/test.test.ts`
- **Test with UI**: `bun run test:ui`
- **Dev server**: `bun run dev:backend` (auto-reload on port 8069)
- **Dev server**: `bun run dev:backend` (gRPC - 8069, HTTP - 8070)
- **Generate protobuf**: `bun run gen` (from proto/ directory)
- **DB migrations**: `bunx drizzle-kit push`

Expand All @@ -20,3 +17,4 @@
- **Testing**: Use Vitest with mocks via `vi.fn()`; mock database transactions in beforeEach; test success, validation errors, and database errors
- **Naming**: camelCase for variables/functions, PascalCase for classes/types/enums, SCREAMING_SNAKE_CASE for constants
- **Database**: Use Drizzle ORM with transactions; validate all inputs before DB operations; handle unique constraint violations explicitly
- **Dates**: Only use the DateTime module, never bother using the built in default date object. Also ALWAYS use utc(), do not try to do any local time fuckery
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ bun run dev:backend
bun start
```

The server will start on `http://localhost:8069`
The server will start on `http://localhost:8070`

## Endpoints

- **Connect / gRPC-Web / gRPC (h2c / HTTP/2 cleartext)**: `http://localhost:8069`
- **Webhook**: `http://localhost:8069/webhooks/lemonsqueezy/createdCheckout`
- **Connect / gRPC-Web / gRPC (h2c / HTTP/2 cleartext)**: `http://localhost:8069` (raw gRPC)
- **Webhook**: `http://localhost:8070/webhooks/lemonsqueezy/createdCheckout`

## Documentation

Expand Down
521 changes: 341 additions & 180 deletions bun.lock

Large diffs are not rendered by default.

23 changes: 13 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,39 @@
"proto:push": "cd proto && git add . && git commit -s -m \"$1\" && git push && cd .."
},
"devDependencies": {
"@bufbuild/protoc-gen-connect-es": "^0.13.0",
"@bufbuild/protoc-gen-es": "^2.9.0",
"@bufbuild/protoc-gen-es": "^2.11.0",
"@types/bun": "latest",
"@types/expr-eval": "^1.1.2",
"@types/luxon": "^3.7.1",
"@vitest/ui": "^4.0.3",
"buf": "bufbuild/buf",
"@bufbuild/buf": "^1.66.1",
"drizzle-kit": "^0.31.6",
"tsx": "^4.20.6",
"skills": "^1.3.1",
"vitest": "^4.0.3"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"@bufbuild/protobuf": "^2.9.0",
"@connectrpc/connect": "^2.1.0",
"@connectrpc/connect-node": "^2.1.0",
"@bufbuild/protobuf": "^2.12.0",
"@connectrpc/connect": "^2.1.1",
"@connectrpc/connect-fastify": "^2.1.1",
"@connectrpc/connect-node": "^2.1.1",
"@connectrpc/protoc-gen-connect-es": "^1.7.0",
"@connectrpc/validate": "^0.2.0",
"@lemonsqueezy/lemonsqueezy.js": "^4.0.0",
"@libsql/client": "^0.15.15",
"@types/expr-eval": "^1.1.2",
"@typescript/native-preview": "^7.0.0-dev.20260502.1",
"bullmq": "^5.75.2",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.44.7",
"expr-eval": "^2.0.2",
"fastify": "^5.8.5",
"fastify-raw-body": "^5.0.0",
"luxon": "^3.7.2",
"mysql2": "^3.15.3",
"pino": "^10.1.0",
"pino-pretty": "^13.1.2",
"postgres": "^3.4.7",
"skills": "^1.3.1",
"zod": "^4.1.12"
}
}
2 changes: 1 addition & 1 deletion proto
Submodule proto updated 1 files
+38 −36 event/v1/event.proto
73 changes: 73 additions & 0 deletions src/errors/internals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Code, ConnectError } from "@connectrpc/connect";

export enum InternalsErrorType {
INVALID_CRON = "INVALID_CRON",
QUEUE_CREATION_FAILED = "QUEUE_CREATION_FAILED",
VALIDATION_FAILED = "VALIDATION_FAILED",
UNKNOWN = "UNKNOWN",
}

export interface InternalsErrorContext {
type: InternalsErrorType;
message: string;
originalError?: Error;
code: Code;
}

export class InternalsError extends ConnectError {
readonly type: InternalsErrorType;
readonly originalError?: Error;

constructor(context: InternalsErrorContext) {
super(context.message, context.code);
this.name = "InternalsError";
this.type = context.type;
this.originalError = context.originalError;

Object.setPrototypeOf(this, InternalsError.prototype);
}

static invalidCron(details?: string, originalError?: Error): InternalsError {
return new InternalsError({
type: InternalsErrorType.INVALID_CRON,
message: details
? `Invalid cron expression: ${details}`
: "Invalid cron expression",
code: Code.InvalidArgument,
originalError,
});
}

static queueCreationFailed(
details?: string,
originalError?: Error
): InternalsError {
return new InternalsError({
type: InternalsErrorType.QUEUE_CREATION_FAILED,
message: details
? `Failed to create queue: ${details}`
: "Failed to create queue",
code: Code.Internal,
originalError,
});
}

static validationFailed(details: string, originalError?: Error): InternalsError {
return new InternalsError({
type: InternalsErrorType.VALIDATION_FAILED,
message: `Internals validation failed: ${details}`,
code: Code.InvalidArgument,
originalError,
});
}

static unknown(originalError?: Error): InternalsError {
const details = originalError?.message || "No details available";
return new InternalsError({
type: InternalsErrorType.UNKNOWN,
message: `Unexpected internals error: ${details}`,
code: Code.Internal,
originalError,
});
}
}
7 changes: 4 additions & 3 deletions src/events/AIEvents/AITokenUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,23 @@ import { DateTime } from "luxon";
import type { UserId } from "../../config/identifiers";

export class AITokenUsage implements AITokenUsageEvent {
public reported_timestamp: DateTime;
public ingested_timestamp: DateTime;
public readonly type = "AI_TOKEN_USAGE" as const;

constructor(
public userId: UserId,
public reportedTimestamp: DateTime,
public data: AITokenUsageEventData
) {
this.reported_timestamp = DateTime.utc();
this.ingested_timestamp = DateTime.utc();
}

serialize() {
return {
SQL: {
type: this.type,
userId: this.userId,
reported_timestamp: this.reported_timestamp,
reported_timestamp: this.reportedTimestamp,
data: this.data,
},
};
Expand Down
6 changes: 3 additions & 3 deletions src/events/RawEvents/AddKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import type { AddKeyEvent, AddKeyEventData } from "../../interface/event/Event";
import { DateTime } from "luxon";

export class AddKey implements AddKeyEvent {
public reported_timestamp: DateTime;
public ingested_timestamp: DateTime;
public readonly type = "ADD_KEY" as const;

constructor(public data: AddKeyEventData) {
this.reported_timestamp = DateTime.utc();
this.ingested_timestamp = DateTime.utc();
}

serialize() {
return {
SQL: {
type: this.type,
reported_timestamp: this.reported_timestamp,
reported_timestamp: this.ingested_timestamp,
data: this.data,
},
};
Expand Down
24 changes: 24 additions & 0 deletions src/events/RawEvents/Metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type {
MetadataEvent,
MetadataEventData,
} from "../../interface/event/Event";
import { DateTime } from "luxon";

export class Metadata implements MetadataEvent {
public ingested_timestamp: DateTime;
public readonly type = "METADATA" as const;

constructor(public data: MetadataEventData) {
this.ingested_timestamp = DateTime.utc();
}

serialize() {
return {
SQL: {
type: this.type,
reported_timestamp: this.ingested_timestamp,
data: this.data,
},
};
}
}
6 changes: 3 additions & 3 deletions src/events/RawEvents/Payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ import { DateTime } from "luxon";
import type { UserId } from "../../config/identifiers";

export class Payment implements PaymentEvent {
public reported_timestamp: DateTime;
public ingested_timestamp: DateTime;
public readonly type = "PAYMENT" as const;

constructor(
public userId: UserId,
public data: PaymentEventData
) {
this.reported_timestamp = DateTime.utc();
this.ingested_timestamp = DateTime.utc();
}

serialize() {
return {
SQL: {
type: this.type,
userId: this.userId,
reported_timestamp: this.reported_timestamp,
reported_timestamp: this.ingested_timestamp,
data: this.data,
},
};
Expand Down
7 changes: 4 additions & 3 deletions src/events/RawEvents/SDKCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,23 @@ import { DateTime } from "luxon";
import type { UserId } from "../../config/identifiers";

export class SDKCall implements SDKCallEvent {
public reported_timestamp: DateTime;
public ingested_timestamp: DateTime;
public readonly type = "SDK_CALL" as const;

constructor(
public userId: UserId,
public reportedTimestamp: DateTime,
public data: SDKCallEventData
) {
this.reported_timestamp = DateTime.utc();
this.ingested_timestamp = DateTime.utc();
}

serialize() {
return {
SQL: {
type: this.type,
userId: this.userId,
reported_timestamp: this.reported_timestamp,
reported_timestamp: this.reportedTimestamp,
data: this.data,
},
};
Expand Down
9 changes: 3 additions & 6 deletions src/factory/EventStorageAdapterFactory.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import type { EventKind } from "../interface/event/Event.ts";
import { PostgresAdapter } from "../storage/adapter/postgres/postgres.ts";

/**
* StorageAdapterFactory - Facade for the new SQL adapter factory
*
* Maintains backward compatibility while delegating to the new
* dependency-injected SQL adapter factory
*/
export class StorageAdapterFactory {
/**
* Get the appropriate storage adapter for a given event
Expand All @@ -29,6 +23,9 @@ export class StorageAdapterFactory {
case "ADD_KEY": {
return new PostgresAdapter();
}
case "METADATA": {
return new PostgresAdapter();
}
default: {
throw new Error(`Unknown event type: ${RequestType}`);
}
Expand Down
5 changes: 3 additions & 2 deletions src/gen/auth/v1/auth_connect.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @generated by protoc-gen-connect-es v0.13.0 with parameter "target=ts"
// @generated by protoc-gen-connect-es v1.7.0 with parameter "target=ts"
// @generated from file auth/v1/auth.proto (package auth.v1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
Expand All @@ -23,5 +23,6 @@ export const AuthService = {
O: CreateAPIKeyResponse,
kind: MethodKind.Unary,
},
},
}
} as const;

33 changes: 11 additions & 22 deletions src/gen/auth/v1/auth_pb.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file auth/v1/auth.proto (package auth.v1, syntax proto3)
/* eslint-disable */

import type {
GenFile,
GenMessage,
GenService,
} from "@bufbuild/protobuf/codegenv2";
import {
fileDesc,
messageDesc,
serviceDesc,
} from "@bufbuild/protobuf/codegenv2";
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import type { Message } from "@bufbuild/protobuf";

/**
* Describes the file auth/v1/auth.proto.
*/
export const file_auth_v1_auth: GenFile =
/*@__PURE__*/
fileDesc(
"ChJhdXRoL3YxL2F1dGgucHJvdG8SB2F1dGgudjEiNgoTQ3JlYXRlQVBJS2V5UmVxdWVzdBIMCgRuYW1lGAEgASgJEhEKCWV4cGlyZXNJbhgCIAEoAyJsChRDcmVhdGVBUElLZXlSZXNwb25zZRIQCghhcGlLZXlJZBgBIAEoCRIOCgZhcGlLZXkYAiABKAkSDAoEbmFtZRgDIAEoCRIRCgljcmVhdGVkQXQYBCABKAkSEQoJZXhwaXJlc0F0GAUgASgJMlwKC0F1dGhTZXJ2aWNlEk0KDENyZWF0ZUFQSUtleRIcLmF1dGgudjEuQ3JlYXRlQVBJS2V5UmVxdWVzdBodLmF1dGgudjEuQ3JlYXRlQVBJS2V5UmVzcG9uc2UiAGIGcHJvdG8z"
);
export const file_auth_v1_auth: GenFile = /*@__PURE__*/
fileDesc("ChJhdXRoL3YxL2F1dGgucHJvdG8SB2F1dGgudjEiNgoTQ3JlYXRlQVBJS2V5UmVxdWVzdBIMCgRuYW1lGAEgASgJEhEKCWV4cGlyZXNJbhgCIAEoAyJsChRDcmVhdGVBUElLZXlSZXNwb25zZRIQCghhcGlLZXlJZBgBIAEoCRIOCgZhcGlLZXkYAiABKAkSDAoEbmFtZRgDIAEoCRIRCgljcmVhdGVkQXQYBCABKAkSEQoJZXhwaXJlc0F0GAUgASgJMlwKC0F1dGhTZXJ2aWNlEk0KDENyZWF0ZUFQSUtleRIcLmF1dGgudjEuQ3JlYXRlQVBJS2V5UmVxdWVzdBodLmF1dGgudjEuQ3JlYXRlQVBJS2V5UmVzcG9uc2UiAGIGcHJvdG8z");

/**
* @generated from message auth.v1.CreateAPIKeyRequest
Expand All @@ -44,8 +33,7 @@ export type CreateAPIKeyRequest = Message<"auth.v1.CreateAPIKeyRequest"> & {
* Describes the message auth.v1.CreateAPIKeyRequest.
* Use `create(CreateAPIKeyRequestSchema)` to create a new message.
*/
export const CreateAPIKeyRequestSchema: GenMessage<CreateAPIKeyRequest> =
/*@__PURE__*/
export const CreateAPIKeyRequestSchema: GenMessage<CreateAPIKeyRequest> = /*@__PURE__*/
messageDesc(file_auth_v1_auth, 0);

/**
Expand Down Expand Up @@ -82,8 +70,7 @@ export type CreateAPIKeyResponse = Message<"auth.v1.CreateAPIKeyResponse"> & {
* Describes the message auth.v1.CreateAPIKeyResponse.
* Use `create(CreateAPIKeyResponseSchema)` to create a new message.
*/
export const CreateAPIKeyResponseSchema: GenMessage<CreateAPIKeyResponse> =
/*@__PURE__*/
export const CreateAPIKeyResponseSchema: GenMessage<CreateAPIKeyResponse> = /*@__PURE__*/
messageDesc(file_auth_v1_auth, 1);

/**
Expand All @@ -99,5 +86,7 @@ export const AuthService: GenService<{
methodKind: "unary";
input: typeof CreateAPIKeyRequestSchema;
output: typeof CreateAPIKeyResponseSchema;
};
}> = /*@__PURE__*/ serviceDesc(file_auth_v1_auth, 0);
},
}> = /*@__PURE__*/
serviceDesc(file_auth_v1_auth, 0);

Loading
Loading