Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
54 changes: 21 additions & 33 deletions bun.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
"typescript": "^5"
},
"dependencies": {
"@better-auth/core": "1.5.5",
"@better-auth/infra": "^0.1.12",
"@better-auth/core": "^1.6.14",
"@better-auth/infra": "^0.2.13",
"@hono/otel": "^1.1.0",
"@hono/zod-validator": "^0.7.6",
"@kubiks/otel-better-auth": "^2.0.2",
Expand All @@ -77,7 +77,7 @@
"@t3-oss/env-core": "^0.13.10",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.90.19",
"better-auth": "^1.5.5",
"better-auth": "^1.6.14",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-kit": "^0.31.8",
Expand Down
36 changes: 36 additions & 0 deletions server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ The flow is handler → service → queries, with `db` passed down (never grabbe
from context inside a service). Add each file only when the feature grows or you
start sharing — a small feature stays one handler.

**Extract on a pressure, not for symmetry.** The default is a fat handler that
owns its whole route; that locality is the point (one file is the complete
truth — easiest to read, change, and reason about). Reach for a layer only when
a concrete pressure shows up:

- **Sharing** — a query is needed by a second route → pull it into `queries.ts`.
- **Size** — a handler grows past what reads in one screen → pull the logic into
a `service` function.
- **Testability** — you want to exercise a rule without faking HTTP → a `service`
function takes `db` + args (never `c`), so it's unit-testable.

The burden of proof is on the layer, not the handler. **Never add a pass-through
layer** — a `service` that only forwards to one query is a smell; inline it.
Reflexively giving every feature a `service.ts` + `queries.ts` is how you get
ravioli: a three-line route smeared across four files, with indirection you pay
on every read. When in doubt, leave it in the handler.

## API paths

Paths concatenate down the mount tree — each level adds one segment:
Expand All @@ -73,3 +90,22 @@ createRouter().get("/", requireAuth, handler); // demo-trace.ts -> /trace
```

So a feature is prefix-agnostic — moving it is a one-line change in `server.ts`.

## Database migrations

Schema lives in `database/schema`. The path from a schema edit to staging is
split on purpose:

1. **Local dev → `db:push`.** Edit the schema, run `bun run db:push`, Drizzle
syncs your local DB to match. No migration files — push is for fast iteration
while the shape is still moving.
1. **Branch ready → `db:generate`.** Once the schema has settled, run
`bun run db:generate` to emit the SQL migration into `database/migrations`
and commit it. That committed SQL is the reviewable, tracked artifact —
generate *once*, at the end, not per tweak.
1. **Staging/prod → automatic.** Migrations apply on server boot via
`runMigrations()` (see `server.ts`), so deploying the branch applies the
committed migration. Nothing manual.

The rule of thumb: **never `generate` mid-dev.** Push while iterating, generate
once when the branch is ready, let the deploy apply it.
Comment thread
drobilc marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
CREATE TABLE "project" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"owner_id" uuid NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "account" (
"id" text PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL,
"account_id" text NOT NULL,
"provider_id" text NOT NULL,
"user_id" text NOT NULL,
"user_id" uuid NOT NULL,
"access_token" text,
"refresh_token" text,
"id_token" text,
Expand All @@ -15,37 +23,44 @@ CREATE TABLE "account" (
);
--> statement-breakpoint
CREATE TABLE "session" (
"id" text PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL,
"expires_at" timestamp NOT NULL,
"token" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp NOT NULL,
"ip_address" text,
"user_agent" text,
"user_id" text NOT NULL,
"user_id" uuid NOT NULL,
"impersonated_by" text,
CONSTRAINT "session_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "user" (
"id" text PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
"email_verified" boolean DEFAULT false NOT NULL,
"image" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"role" text,
"banned" boolean DEFAULT false,
"ban_reason" text,
"ban_expires" timestamp,
"last_active_at" timestamp,
CONSTRAINT "user_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "verification" (
"id" text PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "project" ADD CONSTRAINT "project_owner_id_user_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "account_userId_idx" ON "account" USING btree ("user_id");--> statement-breakpoint
Expand Down
15 changes: 0 additions & 15 deletions server/database/migrations/0001_fat_black_bird.sql

This file was deleted.

6 changes: 0 additions & 6 deletions server/database/migrations/0002_shocking_proteus.sql

This file was deleted.

119 changes: 108 additions & 11 deletions server/database/migrations/meta/0000_snapshot.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,75 @@
{
"id": "709af286-11c5-4d75-820c-3ad308379e09",
"id": "134fd2dc-a67f-4eb8-abfc-fd9e15f46929",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.project": {
"name": "project",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"owner_id": {
"name": "owner_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"project_owner_id_user_id_fk": {
"name": "project_owner_id_user_id_fk",
"tableFrom": "project",
"tableTo": "user",
"columnsFrom": ["owner_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "pg_catalog.gen_random_uuid()"
},
"account_id": {
"name": "account_id",
Expand All @@ -28,7 +85,7 @@
},
"user_id": {
"name": "user_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
Expand Down Expand Up @@ -128,9 +185,10 @@
"columns": {
"id": {
"name": "id",
"type": "text",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "pg_catalog.gen_random_uuid()"
},
"expires_at": {
"name": "expires_at",
Expand Down Expand Up @@ -171,9 +229,15 @@
},
"user_id": {
"name": "user_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"impersonated_by": {
"name": "impersonated_by",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
Expand Down Expand Up @@ -222,9 +286,10 @@
"columns": {
"id": {
"name": "id",
"type": "text",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "pg_catalog.gen_random_uuid()"
},
"name": {
"name": "name",
Expand Down Expand Up @@ -264,6 +329,37 @@
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": false
},
"banned": {
"name": "banned",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"ban_reason": {
"name": "ban_reason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"ban_expires": {
"name": "ban_expires",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"last_active_at": {
"name": "last_active_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
Expand All @@ -286,9 +382,10 @@
"columns": {
"id": {
"name": "id",
"type": "text",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "pg_catalog.gen_random_uuid()"
},
"identifier": {
"name": "identifier",
Expand Down
Loading
Loading