@everystack/mcp 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +681 -0
  2. package/README.md +45 -10
  3. package/dist/adding-database.md +169 -0
  4. package/dist/admin.md +81 -0
  5. package/dist/auth.md +115 -0
  6. package/dist/aws-setup.md +276 -0
  7. package/dist/cli.md +108 -0
  8. package/dist/client-api.md +145 -0
  9. package/dist/core.md +196 -0
  10. package/dist/deployment.md +146 -0
  11. package/dist/events.md +87 -0
  12. package/dist/first-run.md +100 -0
  13. package/dist/getting-started.md +75 -0
  14. package/dist/handler-options.md +114 -0
  15. package/dist/images.md +93 -0
  16. package/dist/index.cjs +23726 -0
  17. package/dist/jobs.md +97 -0
  18. package/dist/logging.md +91 -0
  19. package/dist/plugins.md +68 -0
  20. package/dist/project-claude-md.md +102 -0
  21. package/dist/query-protocol.md +129 -0
  22. package/dist/schema-patterns.md +167 -0
  23. package/dist/security-device.md +99 -0
  24. package/dist/security.md +270 -0
  25. package/dist/ssr.md +82 -0
  26. package/dist/storage.md +63 -0
  27. package/dist/testing.md +118 -0
  28. package/package.json +26 -14
  29. package/src/gates/detectors/embedded-data-bundle.ts +58 -0
  30. package/src/gates/detectors/hand-written-migration.ts +42 -0
  31. package/src/gates/detectors/secret-in-public-env.ts +41 -0
  32. package/src/gates/engine.ts +80 -0
  33. package/src/gates/registry.ts +25 -0
  34. package/src/gates/telemetry.ts +143 -0
  35. package/src/gates/types.ts +70 -0
  36. package/src/governance/cli.ts +193 -0
  37. package/src/governance/grounding.ts +344 -0
  38. package/src/index.ts +97 -50
  39. package/src/prompts/claude-md.ts +90 -0
  40. package/src/prompts/governance-setup.ts +85 -0
  41. package/src/prompts/index.ts +4 -0
  42. package/src/prompts/new-app.ts +4 -1
  43. package/src/resources/project-claude-md.md +69 -94
  44. package/src/tools/index.ts +6 -39
package/dist/jobs.md ADDED
@@ -0,0 +1,97 @@
1
+ # Background Jobs
2
+
3
+ > SQS background workers for everystack apps. Import from `@everystack/jobs`.
4
+
5
+ ## When to Use
6
+ Read this when adding background processing (V3): email sending, image processing, analytics aggregation, etc.
7
+
8
+ ## Setup
9
+
10
+ ### Infrastructure
11
+ ```typescript
12
+ // sst.config.ts
13
+ const dlq = new sst.aws.Queue('DeadLetterQueue');
14
+ const jobs = new sst.aws.Queue('Jobs', { dlq: dlq.arn });
15
+
16
+ const worker = new sst.aws.Function('Worker', {
17
+ handler: 'server/worker.handler',
18
+ link: [database, jobs, media],
19
+ vpc,
20
+ });
21
+
22
+ jobs.subscribe(worker.arn);
23
+ ```
24
+
25
+ ### Worker Handler
26
+ ```typescript
27
+ // server/worker.ts
28
+ import { createWorkerHandler } from '@everystack/server/worker';
29
+
30
+ export const handler = createWorkerHandler(async () => ({
31
+ 'image:process': async (payload) => {
32
+ // payload: { imageId, variants: ['thumb', 'large'] }
33
+ },
34
+ 'email:send': async (payload) => {
35
+ // payload: { to, subject, body }
36
+ },
37
+ 'analytics:aggregate': async (payload) => {
38
+ // payload: { date, metrics: ['pageviews', 'signups'] }
39
+ },
40
+ }));
41
+ ```
42
+
43
+ ### Dispatching Jobs
44
+ ```typescript
45
+ import { publishJob } from '@everystack/jobs';
46
+
47
+ await publishJob('image:process', {
48
+ imageId: 'abc-123',
49
+ variants: ['thumb', 'large'],
50
+ });
51
+ ```
52
+
53
+ ## Client SDK
54
+
55
+ ```typescript
56
+ import { createJobClient } from '@everystack/jobs/client';
57
+
58
+ const jobs = createJobClient({ baseUrl: '/api' });
59
+ await jobs.submit('email:send', { to: 'user@example.com', subject: 'Welcome' });
60
+ ```
61
+
62
+ ## SQS Adapter
63
+
64
+ ```typescript
65
+ import { createSqsAdapter } from '@everystack/jobs';
66
+
67
+ const adapter = createSqsAdapter({
68
+ queueUrl: Resource.Jobs.url,
69
+ region: 'us-east-1',
70
+ });
71
+ ```
72
+
73
+ Progressive dispatch: starts with direct function calls, moves to SQS when you need async processing.
74
+
75
+ ## Postgres Materialization
76
+
77
+ Optional: materialize job state to PostgreSQL for dashboard visibility:
78
+
79
+ ```typescript
80
+ import { jobsSchema } from '@everystack/jobs/schema';
81
+ // Adds: jobs table with status, type, payload, result, timestamps
82
+ ```
83
+
84
+ ## Plugin
85
+
86
+ ```typescript
87
+ import { jobsPlugin } from '@everystack/jobs/plugin';
88
+ // Adds job dispatch routes to the handler
89
+ ```
90
+
91
+ ## Gotchas
92
+
93
+ - Workers run in a separate Lambda (not the API Lambda)
94
+ - Dead letter queue catches failed jobs after max retries
95
+ - Job payloads must be JSON-serializable
96
+ - SQS has a 256KB message size limit
97
+ - Workers should be idempotent (SQS delivers at-least-once)
@@ -0,0 +1,91 @@
1
+ # Logging & Analytics
2
+
3
+ > Structured logging, crash reports, and analytics. Import from `@everystack/logging`.
4
+
5
+ ## When to Use
6
+ Read this when adding observability to a V2+ app.
7
+
8
+ ## Setup
9
+
10
+ ### Server-side Logging
11
+ ```typescript
12
+ import { createLogSink } from '@everystack/logging';
13
+
14
+ const logSink = createLogSink({
15
+ storage: 's3',
16
+ bucket: Resource.LogsBucket.name,
17
+ });
18
+
19
+ // Pass to Lambda handler
20
+ createLambdaHandler({ logSink, /* ... */ });
21
+ ```
22
+
23
+ ### Client-side SDK
24
+ ```typescript
25
+ import { createLogClient } from '@everystack/logging/client';
26
+
27
+ const logger = createLogClient({
28
+ baseUrl: '/api/logs',
29
+ batchSize: 10,
30
+ flushInterval: 30000,
31
+ });
32
+
33
+ logger.info('Page viewed', { page: '/home' });
34
+ logger.error('Payment failed', { error, userId });
35
+ logger.event('button_click', { button: 'signup' });
36
+ ```
37
+
38
+ ## Log Levels
39
+
40
+ | Level | Use case |
41
+ |-------|----------|
42
+ | `debug` | Development-only, verbose details |
43
+ | `info` | Normal operations, user actions |
44
+ | `warn` | Degraded state, retries, fallbacks |
45
+ | `error` | Failed operations, caught exceptions |
46
+ | `fatal` | Unrecoverable errors, crash reports |
47
+
48
+ ## Schema
49
+
50
+ ```typescript
51
+ import { loggingSchema } from '@everystack/logging/schema';
52
+ // Adds: logs table with level, source, message, data, traceId, userId, deviceId, etc.
53
+ ```
54
+
55
+ Include in your handler's schema for database-backed log querying.
56
+
57
+ ## CloudWatch Trigger
58
+
59
+ ```typescript
60
+ import { createLogIngestionHandler } from '@everystack/logging/trigger';
61
+ // Processes CloudWatch log events, extracts structured data, stores in S3/DB
62
+ ```
63
+
64
+ ## Admin Integration
65
+
66
+ ```typescript
67
+ import { loggingAdminConfig } from '@everystack/logging/admin';
68
+ // Adds log viewer widget to admin dashboard
69
+ ```
70
+
71
+ ## CLI Integration
72
+
73
+ ```bash
74
+ everystack logs:errors --stage dev # Recent errors (DB -> S3 fallback)
75
+ everystack logs:query --stage dev --level error --source api
76
+ everystack logs:tail --stage dev # Raw CloudWatch output
77
+ ```
78
+
79
+ ## Plugin
80
+
81
+ ```typescript
82
+ import { loggingPlugin } from '@everystack/logging/plugin';
83
+ // Adds log ingestion routes and log query RPC
84
+ ```
85
+
86
+ ## Gotchas
87
+
88
+ - Logs are stored in S3 (cheap, durable) with optional DB materialization
89
+ - Client SDK batches logs and flushes periodically (not per-event)
90
+ - `logs:errors` queries DB first, falls back to S3 if logs table doesn't exist
91
+ - `logs:tail` reads CloudWatch (Lambda container output), not application logs
@@ -0,0 +1,68 @@
1
+ # Plugin Architecture
2
+
3
+ > Composable handler system for everystack packages. Import plugin system from `@everystack/server/plugin`.
4
+
5
+ ## When to Use
6
+ Read this when composing multiple everystack packages into a single Lambda handler.
7
+
8
+ ## Setup
9
+
10
+ ```typescript
11
+ import { createPluginLambdaHandler, type Plugin, type PluginContext } from '@everystack/server/plugin';
12
+
13
+ const handler = createPluginLambdaHandler({
14
+ plugins: [authPlugin, jobsPlugin, storagePlugin, apiPlugin],
15
+ schema,
16
+ // ... other options
17
+ });
18
+ ```
19
+
20
+ ## Plugin Type
21
+
22
+ ```typescript
23
+ type Plugin = (ctx: PluginContext) => Promise<Record<string, Handler>>;
24
+
25
+ interface PluginContext {
26
+ db: DrizzleDb;
27
+ schema: Record<string, Table>;
28
+ verifyToken: (token: string) => Promise<Record<string, unknown> | null>;
29
+ }
30
+ ```
31
+
32
+ A plugin receives context and returns a map of named handlers.
33
+
34
+ ## Available Plugins
35
+
36
+ | Plugin | Package | Routes |
37
+ |--------|---------|--------|
38
+ | `authPlugin` | `@everystack/auth/plugin` | signup, signin, refresh, verify, signout |
39
+ | `jobsPlugin` | `@everystack/jobs/plugin` | job dispatch, status |
40
+ | `storagePlugin` | `@everystack/storage/plugin` | upload, confirm, list, delete |
41
+ | `loggingPlugin` | `@everystack/logging/plugin` | log ingestion, query |
42
+ | `cliPlugin` | `@everystack/cli/plugin` | OTA updates, manifest |
43
+
44
+ ## Writing a Custom Plugin
45
+
46
+ ```typescript
47
+ const myPlugin: Plugin = async (ctx) => {
48
+ return {
49
+ myHandler: async (request: Request) => {
50
+ // Use ctx.db, ctx.schema, ctx.verifyToken
51
+ return new Response(JSON.stringify({ ok: true }));
52
+ },
53
+ };
54
+ };
55
+ ```
56
+
57
+ ## Plugin vs Standalone Routes
58
+
59
+ **Plugins:** Compose into a single Lambda handler. Share db connection pool and auth. Use for production.
60
+
61
+ **Standalone Expo Router routes:** Each route is a separate file. Filesystem-based fallbacks for local dev. Use when plugins depend on AWS resources not available locally.
62
+
63
+ ## Gotchas
64
+
65
+ - Plugin registration order matters for route matching
66
+ - Plugins share the same database connection pool
67
+ - Each plugin's handlers are merged into the route table
68
+ - The API handler (PostgREST) should be the last plugin (catch-all)
@@ -0,0 +1,102 @@
1
+ # {PROJECT_NAME}
2
+
3
+ > {ONE_LINE_DESCRIPTION}
4
+
5
+ This file is the contract for how this project is built. Read it before writing code; it
6
+ overrides convenience. When a rule is hard to follow, that is the signal you are about to
7
+ introduce drift — stop and do it the right way.
8
+
9
+ <!--
10
+ REQUIRED-READS: (optional) a comma-separated list of companion files that must also be
11
+ read before non-Read tools, e.g. `REQUIRED-READS: docs/architecture.md, db/SCHEMA.md`.
12
+ This CLAUDE.md is required automatically — add the line only if real companion docs exist.
13
+ -->
14
+
15
+ ## Non-negotiables
16
+
17
+ These are enforced (everystack cheat gates) and load-bearing. Do not work around them.
18
+
19
+ - **Data lives in PostgreSQL, served through the API — never bundle data into the app.** A
20
+ large `.json`/`.csv` of computed data in the bundle is wrong; model it and serve it, or
21
+ render an empty state if it does not exist yet.
22
+ - **Schema and migrations are generated from Models.** Declare tables with `defineModel`
23
+ (in `models/`), run `everystack db:generate`. Never hand-write a SQL migration, never edit
24
+ the generated `db/schema.ts`. After any change, `db:generate` must be a clean no-op.
25
+ - **Authorization is declared, not hand-written.** Use `can()` abilities on the Model; they
26
+ compile to RLS + grants. Never hand-write `CREATE POLICY`/`GRANT`. RLS is required.
27
+ - **Reuse `@everystack/ui`.** Do not hand-roll a component that already exists there. Style
28
+ with classNames (light + dark), not inline `StyleSheet`.
29
+ - **Secrets stay server-side.** Never put a DB URL, key, or token behind an `EXPO_PUBLIC_*`
30
+ name — those are compiled into the client bundle forever. Use `everystack secrets`.
31
+ - **Apps stay thin.** Screens wire routing and UI; reusable logic lives in packages/`lib`.
32
+
33
+ ## Start Here
34
+
35
+ - `models/` — `defineModel` tables (the source of truth for schema + authz)
36
+ - `app/` — Expo Router pages (screens, navigation, API routes)
37
+ - `server/` — Lambda handlers (api.ts, worker.ts, image.ts)
38
+ - `db/` — generated Drizzle schema + migrations (do not edit by hand)
39
+ - `lib/` — shared code (auth context, API client)
40
+ - `sst.config.ts` — AWS infrastructure definition
41
+
42
+ ## Structure
43
+
44
+ {ANNOTATED_DIRECTORY_TREE}
45
+
46
+ ## Commands
47
+
48
+ ```bash
49
+ pnpm install # Install dependencies
50
+ pnpm dev # Start the Expo dev server
51
+ pnpm test # Run all tests (TDD)
52
+ everystack db:generate # Models → next migration (data + authz)
53
+ everystack db:migrate # Apply migrations on the deployed Lambda
54
+ everystack db:seed # Seed the database (dev only)
55
+ everystack deploy --stage dev # Deploy infrastructure (SST)
56
+ everystack update --channel production # OTA update (no redeploy)
57
+ everystack secrets set KEY "value" --stage dev # Set a server-side secret
58
+ everystack bundle:audit <url> # Audit a deployed bundle (weight + leaks)
59
+ ```
60
+
61
+ ## Key Principles
62
+
63
+ ### Models are the source of truth (v3)
64
+
65
+ Declare tables with `defineModel` (`field`, `can`, relations). A package's full DB slice is a
66
+ `defineModule`; the app composes Modules. `everystack db:generate` compiles them to one
67
+ migration (schema + RLS + grants); `deriveHandlerConfig(models)` derives the API config. You
68
+ never hand-write migrations, RLS, or handler access-control — they are derived, so they cannot
69
+ drift.
70
+
71
+ ### Security over all else
72
+
73
+ Three layers, each a complete boundary: edge (CloudFront JWT check), handler (pgSettings role +
74
+ ownership), and database (GRANTs + RLS — the source of truth). Even if the handler has a bug,
75
+ the database enforces access. Never skip RLS. Never connect as superuser in production.
76
+
77
+ ### Progressive complexity
78
+
79
+ Each tier adds to the previous; you never rip out what you have.
80
+ - **V1:** Static. Expo + S3 + CloudFront + Lambda SSR + OTA. No database.
81
+ - **V2:** Add PostgreSQL, the API, JWT auth, admin, logging.
82
+ - **V3:** Add SQS workers, image processing, S3 file storage.
83
+
84
+ ### Test-driven
85
+
86
+ Every feature starts with a failing test. Tests in `__tests__/` mirroring source.
87
+
88
+ ## Conventions
89
+
90
+ - Package scope `@everystack/*`; files `kebab-case.ts`; named exports (no defaults); types
91
+ PascalCase (no `I` prefix); functions camelCase; DB columns `snake_case`.
92
+ - TypeScript strict; prefer `unknown` over `any`; explicit return types on exports.
93
+ - Conventional commits (`feat:`, `fix:`, `chore:`, `docs:`); tests pass before commit.
94
+
95
+ ## What NOT to Do
96
+
97
+ - Don't bundle large data into the app — it lives in the DB, served by the API.
98
+ - Don't hand-write migrations or edit `db/schema.ts` — edit the Model, run `db:generate`.
99
+ - Don't hand-write RLS — declare `can()` abilities.
100
+ - Don't hand-roll a component that exists in `@everystack/ui`; don't use inline `StyleSheet`.
101
+ - Don't put a secret behind `EXPO_PUBLIC_*` — that ships to the client.
102
+ - Don't skip RLS, skip tests, or build features beyond what's tested.
@@ -0,0 +1,129 @@
1
+ # Query Protocol
2
+
3
+ > PostgREST-compatible HTTP query protocol used by the everystack handler.
4
+
5
+ ## When to Use
6
+ Read this when building queries (client-side or debugging API calls). The handler converts these HTTP patterns into Drizzle ORM queries.
7
+
8
+ ## Filters
9
+
10
+ Format: `?column=operator.value`
11
+
12
+ | Operator | SQL | Example |
13
+ |----------|-----|---------|
14
+ | `eq` | `=` | `?id=eq.5` |
15
+ | `neq` | `!=` | `?status=neq.deleted` |
16
+ | `gt` | `>` | `?price=gt.100` |
17
+ | `gte` | `>=` | `?age=gte.18` |
18
+ | `lt` | `<` | `?price=lt.50` |
19
+ | `lte` | `<=` | `?rating=lte.3` |
20
+ | `like` | `LIKE` | `?title=like.*hiking*` (use `*` for wildcard) |
21
+ | `ilike` | `ILIKE` | `?name=ilike.*john*` |
22
+ | `is` | `IS NULL/NOT NULL` | `?deleted=is.null` or `?email=is.not.null` |
23
+ | `in` | `IN (...)` | `?status=in.(draft,published)` |
24
+
25
+ Multiple filters on the same request are ANDed: `?status=eq.published&authorId=eq.5`.
26
+
27
+ Strict validation: unknown columns or operators return 400 (not silently ignored).
28
+
29
+ ## Negation
30
+
31
+ Prefix any operator with `not.`:
32
+ ```
33
+ ?status=not.eq.draft -> status != 'draft'
34
+ ?tags=not.in.(a,b) -> tags NOT IN ('a', 'b')
35
+ ?deletedAt=not.is.null -> deleted_at IS NOT NULL
36
+ ```
37
+
38
+ ## Logical Groups
39
+
40
+ ```
41
+ ?or=(status.eq.draft,status.eq.pending)
42
+ -> WHERE status = 'draft' OR status = 'pending'
43
+
44
+ ?and=(price.gte.10,price.lte.100)
45
+ -> WHERE price >= 10 AND price <= 100
46
+
47
+ ?category=eq.electronics&or=(status.eq.draft,status.eq.pending)
48
+ -> WHERE category = 'electronics' AND (status = 'draft' OR status = 'pending')
49
+ ```
50
+
51
+ Negation works inside groups: `?or=(category.not.eq.electronics,price.gt.100)`.
52
+
53
+ ## Column Selection
54
+
55
+ ```
56
+ ?select=id,title,body -> Returns only { id, title, body }
57
+ ```
58
+
59
+ ## Relation Embedding
60
+
61
+ ```
62
+ ?select=*,author(*) -> Each post includes { author: { ... } }
63
+ ?select=*,author(name,email) -> Specific columns from relation
64
+ ?select=*,author(*),comments(*) -> Multiple relations
65
+ ```
66
+
67
+ Requires `relations` configured in handler options. Resolved via batched `WHERE IN` (not N+1).
68
+
69
+ ## Ordering
70
+
71
+ ```
72
+ ?order=created_at.desc
73
+ ?order=title.asc
74
+ ```
75
+
76
+ ## Pagination
77
+
78
+ ```
79
+ ?limit=10&offset=0 -> First page
80
+ ?limit=10&offset=10 -> Second page
81
+ ```
82
+
83
+ Default limit: 1000. Max limit configurable via `maxLimit` option.
84
+
85
+ ## Exact Count
86
+
87
+ Send `Prefer: count=exact` header to get total row count in `Content-Range`:
88
+ ```
89
+ Content-Range: 0-9/42 (10 rows, 42 total)
90
+ ```
91
+
92
+ ## Aggregates
93
+
94
+ ```
95
+ ?select=count() -> SELECT count(*)
96
+ ?select=platform,total:amount.sum() -> SELECT platform, sum(amount) AS total GROUP BY platform
97
+ ?select=count(distinct authorId) -> SELECT count(DISTINCT author_id)
98
+ ```
99
+
100
+ Supported: `count()`, `sum()`, `avg()`, `min()`, `max()`. Non-aggregate columns in select become GROUP BY columns. Alias syntax: `alias:column.function()`.
101
+
102
+ ## JSON Path Filtering
103
+
104
+ ```
105
+ ?metadata->>theme=eq.dark -> metadata->>'theme' = 'dark'
106
+ ?data->settings->>mode=eq.compact -> data->'settings'->>'mode' = 'compact'
107
+ ```
108
+
109
+ `->` returns JSON object (for nesting), `->>` returns text (for comparisons). Keys validated against `^[a-zA-Z_][a-zA-Z0-9_]*$`.
110
+
111
+ ## Mutations
112
+
113
+ **POST** (insert): `POST /table` with JSON body. Returns created row, status 201.
114
+
115
+ **PATCH** (update): `PATCH /table?filters` with JSON body. Returns updated rows, status 200.
116
+
117
+ **DELETE**: `DELETE /table?filters`. Requires at least one filter (400 without). Returns deleted rows.
118
+
119
+ **RPC**: `POST /rpc/name` with JSON body. Returns function result.
120
+
121
+ ## Content-Range Header
122
+
123
+ Every GET response includes `Content-Range`:
124
+ ```
125
+ 0-9/* 10 rows, total unknown
126
+ 0-9/42 10 rows, 42 total (with Prefer: count=exact)
127
+ */* no rows, total unknown
128
+ */0 no rows, 0 total
129
+ ```
@@ -0,0 +1,167 @@
1
+ # Schema Design Patterns
2
+
3
+ > Drizzle schema design, migrations, and RLS setup for everystack apps.
4
+
5
+ ## When to Use
6
+ Read this when designing your database schema or adding tables to an existing app.
7
+
8
+ ## Schema Location
9
+
10
+ Schemas live in `db/schema.ts` (or `db/schema/` directory for larger apps). This file is the single source of truth for both the handler and SSR.
11
+
12
+ ## Basic Table
13
+
14
+ ```typescript
15
+ import { pgTable, serial, text, integer, timestamp, uuid } from 'drizzle-orm/pg-core';
16
+
17
+ export const posts = pgTable('posts', {
18
+ id: uuid('id').defaultRandom().primaryKey(),
19
+ body: text('body').notNull(),
20
+ authorId: uuid('author_id').notNull().references(() => users.id),
21
+ status: text('status').default('draft').notNull(),
22
+ createdAt: timestamp('created_at').defaultNow().notNull(),
23
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
24
+ deletedAt: timestamp('deleted_at'),
25
+ });
26
+ ```
27
+
28
+ ## Common Patterns
29
+
30
+ ### UUID vs Serial Primary Keys
31
+ Prefer UUID for user-facing IDs (prevents enumeration attacks). Use serial for internal-only tables.
32
+
33
+ ```typescript
34
+ id: uuid('id').defaultRandom().primaryKey(), // Preferred for API-exposed tables
35
+ id: serial('id').primaryKey(), // OK for internal tables
36
+ ```
37
+
38
+ ### Timestamps
39
+ Always include `createdAt` and `updatedAt`:
40
+ ```typescript
41
+ createdAt: timestamp('created_at').defaultNow().notNull(),
42
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
43
+ ```
44
+
45
+ ### Soft Delete
46
+ Add `deletedAt` for soft-deletable tables:
47
+ ```typescript
48
+ deletedAt: timestamp('deleted_at'),
49
+ ```
50
+ Configure in handler: `softDelete: { column: 'deletedAt', tables: ['posts'] }`.
51
+
52
+ ### Foreign Keys
53
+ ```typescript
54
+ authorId: uuid('author_id').notNull().references(() => users.id),
55
+ ```
56
+
57
+ ### Enums
58
+ ```typescript
59
+ import { pgEnum } from 'drizzle-orm/pg-core';
60
+
61
+ export const roleEnum = pgEnum('role', ['user', 'admin']);
62
+
63
+ export const users = pgTable('users', {
64
+ role: roleEnum('role').default('user').notNull(),
65
+ });
66
+ ```
67
+
68
+ ## Relations (Drizzle)
69
+
70
+ ```typescript
71
+ import { relations } from 'drizzle-orm';
72
+
73
+ export const postsRelations = relations(posts, ({ one, many }) => ({
74
+ author: one(users, { fields: [posts.authorId], references: [users.id] }),
75
+ comments: many(comments),
76
+ }));
77
+ ```
78
+
79
+ Drizzle relations are used by the relational query API (`db.query.posts.findMany({ with: { author: true } })`). The handler also needs its own `relations` config for embedding.
80
+
81
+ ## Indexes
82
+
83
+ ```typescript
84
+ import { index, uniqueIndex } from 'drizzle-orm/pg-core';
85
+
86
+ export const posts = pgTable('posts', {
87
+ // columns...
88
+ }, (table) => [
89
+ index('posts_author_id_idx').on(table.authorId),
90
+ uniqueIndex('posts_slug_idx').on(table.slug),
91
+ ]);
92
+ ```
93
+
94
+ ## Migration Workflow
95
+
96
+ ```bash
97
+ # Generate migration from schema changes
98
+ npx drizzle-kit generate
99
+
100
+ # Apply migrations locally
101
+ npx drizzle-kit migrate
102
+
103
+ # Apply migrations on deployed Lambda
104
+ everystack db:migrate
105
+ ```
106
+
107
+ Migration files go in `drizzle/` directory. Each migration is a SQL file.
108
+
109
+ ### RLS Migration Template
110
+
111
+ After creating tables, add a migration for roles and policies:
112
+
113
+ ```sql
114
+ -- Create roles (idempotent)
115
+ DO $$ BEGIN
116
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authenticator') THEN
117
+ CREATE ROLE authenticator LOGIN NOINHERIT;
118
+ END IF;
119
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'anon') THEN
120
+ CREATE ROLE anon NOLOGIN;
121
+ END IF;
122
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authenticated') THEN
123
+ CREATE ROLE authenticated NOLOGIN;
124
+ END IF;
125
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'admin') THEN
126
+ CREATE ROLE admin NOLOGIN;
127
+ END IF;
128
+ END $$;
129
+
130
+ GRANT anon TO authenticator;
131
+ GRANT authenticated TO authenticator;
132
+ GRANT admin TO authenticator;
133
+
134
+ -- Table grants
135
+ GRANT USAGE ON SCHEMA public TO anon, authenticated, admin;
136
+ GRANT SELECT ON posts TO anon;
137
+ GRANT SELECT, INSERT, UPDATE, DELETE ON posts TO authenticated;
138
+ GRANT SELECT, INSERT, UPDATE, DELETE ON posts TO admin;
139
+
140
+ -- Enable RLS
141
+ ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
142
+ ALTER TABLE posts FORCE ROW LEVEL SECURITY;
143
+
144
+ -- Policies (see everystack://security for templates)
145
+ ```
146
+
147
+ ## Handler Config for Schema
148
+
149
+ ```typescript
150
+ createHandler(db, schema, {
151
+ exposedTables: ['posts', 'profiles'], // Only these are API-accessible
152
+ hiddenColumns: { users: ['passwordHash', 'resetToken'] },
153
+ protectedFields: { profiles: ['role', 'deletedAt'] },
154
+ relations: {
155
+ posts: { author: { table: 'users', from: 'authorId', to: 'id' } },
156
+ },
157
+ });
158
+ ```
159
+
160
+ ## Gotchas
161
+
162
+ - Drizzle column names (camelCase) and SQL column names (snake_case) are both specified: `authorId: uuid('author_id')`
163
+ - The handler's `relations` config uses Drizzle property names, not SQL column names
164
+ - `exposedTables` uses Drizzle table variable names, not SQL table names
165
+ - Always run `drizzle-kit generate` after schema changes
166
+ - RLS migration must be run AFTER table creation migration
167
+ - Connect as `authenticator` in production (never RDS master/superuser)