@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.
- package/LICENSE +681 -0
- package/README.md +45 -10
- package/dist/adding-database.md +169 -0
- package/dist/admin.md +81 -0
- package/dist/auth.md +115 -0
- package/dist/aws-setup.md +276 -0
- package/dist/cli.md +108 -0
- package/dist/client-api.md +145 -0
- package/dist/core.md +196 -0
- package/dist/deployment.md +146 -0
- package/dist/events.md +87 -0
- package/dist/first-run.md +100 -0
- package/dist/getting-started.md +75 -0
- package/dist/handler-options.md +114 -0
- package/dist/images.md +93 -0
- package/dist/index.cjs +23726 -0
- package/dist/jobs.md +97 -0
- package/dist/logging.md +91 -0
- package/dist/plugins.md +68 -0
- package/dist/project-claude-md.md +102 -0
- package/dist/query-protocol.md +129 -0
- package/dist/schema-patterns.md +167 -0
- package/dist/security-device.md +99 -0
- package/dist/security.md +270 -0
- package/dist/ssr.md +82 -0
- package/dist/storage.md +63 -0
- package/dist/testing.md +118 -0
- package/package.json +26 -14
- package/src/gates/detectors/embedded-data-bundle.ts +58 -0
- package/src/gates/detectors/hand-written-migration.ts +42 -0
- package/src/gates/detectors/secret-in-public-env.ts +41 -0
- package/src/gates/engine.ts +80 -0
- package/src/gates/registry.ts +25 -0
- package/src/gates/telemetry.ts +143 -0
- package/src/gates/types.ts +70 -0
- package/src/governance/cli.ts +193 -0
- package/src/governance/grounding.ts +344 -0
- package/src/index.ts +97 -50
- package/src/prompts/claude-md.ts +90 -0
- package/src/prompts/governance-setup.ts +85 -0
- package/src/prompts/index.ts +4 -0
- package/src/prompts/new-app.ts +4 -1
- package/src/resources/project-claude-md.md +69 -94
- 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)
|
package/dist/logging.md
ADDED
|
@@ -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
|
package/dist/plugins.md
ADDED
|
@@ -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)
|