diff --git a/.gitignore b/.gitignore index 829b525..5f053bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +# Keeps local dependencies, secrets, build output, and editor state out of Git. node_modules/ dist/ coverage/ @@ -11,4 +12,3 @@ coverage/ Thumbs.db .idea/ .vscode/ -apps/api/drizzle/ diff --git a/apps/api/drizzle.config.ts b/apps/api/drizzle.config.ts index e50da09..ae1633c 100644 --- a/apps/api/drizzle.config.ts +++ b/apps/api/drizzle.config.ts @@ -1,3 +1,4 @@ +/** Configures Drizzle Kit schema discovery and PostgreSQL migration output. */ import 'dotenv/config'; import { defineConfig } from 'drizzle-kit'; @@ -7,7 +8,7 @@ if (!process.env.DATABASE_URL) { export default defineConfig({ dialect: 'postgresql', - schema: './src/db/schema.ts', + schema: './src/db/schema/index.ts', out: './drizzle', dbCredentials: { url: process.env.DATABASE_URL }, strict: true, diff --git a/apps/api/drizzle/0000_curved_salo.sql b/apps/api/drizzle/0000_curved_salo.sql new file mode 100644 index 0000000..e1065e4 --- /dev/null +++ b/apps/api/drizzle/0000_curved_salo.sql @@ -0,0 +1,7 @@ +CREATE TABLE "sessions" ( + "sid" varchar PRIMARY KEY NOT NULL, + "sess" json NOT NULL, + "expire" timestamp (6) NOT NULL +); +--> statement-breakpoint +CREATE INDEX "sessions_expire_idx" ON "sessions" USING btree ("expire"); \ No newline at end of file diff --git a/apps/api/drizzle/meta/0000_snapshot.json b/apps/api/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..e149896 --- /dev/null +++ b/apps/api/drizzle/meta/0000_snapshot.json @@ -0,0 +1,66 @@ +{ + "id": "14e1489f-6ad1-4119-aad3-bb8e2db293a3", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "sid": { + "name": "sid", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "sess": { + "name": "sess", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "expire": { + "name": "expire", + "type": "timestamp (6)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "sessions_expire_idx": { + "name": "sessions_expire_idx", + "columns": [ + { + "expression": "expire", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json new file mode 100644 index 0000000..97a78e3 --- /dev/null +++ b/apps/api/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1787007119704, + "tag": "0000_curved_salo", + "breakpoints": true + } + ] +} diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts deleted file mode 100644 index 8da6f66..0000000 --- a/apps/api/src/db/schema.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Product tables are intentionally deferred until their corresponding features. -export {}; diff --git a/apps/api/src/db/schema/index.ts b/apps/api/src/db/schema/index.ts new file mode 100644 index 0000000..a350bf2 --- /dev/null +++ b/apps/api/src/db/schema/index.ts @@ -0,0 +1,2 @@ +/** Exposes the complete Drizzle schema from feature-focused table modules. */ +export * from './sessions.js'; diff --git a/apps/api/src/db/schema/sessions.ts b/apps/api/src/db/schema/sessions.ts new file mode 100644 index 0000000..9fbc013 --- /dev/null +++ b/apps/api/src/db/schema/sessions.ts @@ -0,0 +1,12 @@ +/** Defines the PostgreSQL table used for server-side authentication sessions. */ +import { index, json, pgTable, timestamp, varchar } from 'drizzle-orm/pg-core'; + +export const sessions = pgTable( + 'sessions', + { + sid: varchar('sid').primaryKey(), + sess: json('sess').notNull(), + expire: timestamp('expire', { mode: 'date', precision: 6 }).notNull(), + }, + (table) => [index('sessions_expire_idx').on(table.expire)], +); diff --git a/docs/database.md b/docs/database.md index dafef19..27a552b 100644 --- a/docs/database.md +++ b/docs/database.md @@ -114,23 +114,24 @@ in the MVP. | Field | Type | Purpose | |---|---|---| -| `id` | UUID or sufficiently strong opaque identifier | Session identifier placed in the browser cookie. If the implementation stores only a hash of the cookie value, this field contains that hash instead. | -| `user_id` | UUID, FK → `users.id` | User authenticated by the session. | -| `expires_at` | TIMESTAMPTZ | Absolute time after which the session is invalid. | -| `created_at` | TIMESTAMPTZ | When the session was created. | -| `last_accessed_at` | TIMESTAMPTZ, nullable | Optional timestamp for idle-expiration or session-management behavior if adopted during implementation. | +| `sid` | VARCHAR | Opaque session identifier used by `connect-pg-simple`. | +| `sess` | JSON | Serialized server-side session state, including the authenticated GoForLift user ID after login. | +| `expire` | TIMESTAMP(6) | Absolute time after which the session is invalid. | ### Constraints and Lifecycle -- `id` is the primary key or uniquely indexed lookup value. -- `user_id` references `users.id`. +- `sid` is the primary key and session lookup index. +- The table shape intentionally matches `connect-pg-simple`; application user + identity is stored inside `sess` rather than in a separate normalized column. - Session identifiers must be generated using a cryptographically secure source. - Expired sessions must not authenticate requests and should be removed - periodically. + at least daily by the configured session store. - Logout deletes or invalidates the matching session. -- Deleting a user must invalidate that user's sessions. -- An index on `user_id` supports user-wide session revocation. -- An index on `expires_at` may support expiration cleanup. +- Deleting a user must invalidate that user's sessions through application-owned + session cleanup. +- An index on `expire` supports expiration pruning. +- Drizzle owns table creation and migration. Runtime table creation through + `connect-pg-simple` remains disabled. # 5. Exercises @@ -619,9 +620,7 @@ Likely query patterns should guide additional indexes. Potential candidates include: - sessions.user_id - - sessions.expires_at + sessions.expire routines.user_id