@everystack/mcp 0.2.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/README.md +100 -0
- package/package.json +39 -0
- package/src/index.ts +58 -0
- package/src/prompts/add-feature.ts +163 -0
- package/src/prompts/debug.ts +136 -0
- package/src/prompts/deploy.ts +131 -0
- package/src/prompts/design-schema.ts +104 -0
- package/src/prompts/index.ts +16 -0
- package/src/prompts/new-app.ts +211 -0
- package/src/prompts/secure.ts +231 -0
- package/src/resources/adding-database.md +169 -0
- package/src/resources/admin.md +81 -0
- package/src/resources/auth.md +115 -0
- package/src/resources/aws-setup.md +173 -0
- package/src/resources/cli.md +108 -0
- package/src/resources/client-api.md +145 -0
- package/src/resources/core.md +196 -0
- package/src/resources/deployment.md +146 -0
- package/src/resources/events.md +87 -0
- package/src/resources/first-run.md +100 -0
- package/src/resources/getting-started.md +75 -0
- package/src/resources/handler-options.md +114 -0
- package/src/resources/images.md +73 -0
- package/src/resources/index.ts +224 -0
- package/src/resources/jobs.md +97 -0
- package/src/resources/logging.md +91 -0
- package/src/resources/plugins.md +68 -0
- package/src/resources/project-claude-md.md +127 -0
- package/src/resources/query-protocol.md +129 -0
- package/src/resources/schema-patterns.md +167 -0
- package/src/resources/security-device.md +99 -0
- package/src/resources/security.md +270 -0
- package/src/resources/ssr.md +82 -0
- package/src/resources/storage.md +63 -0
- package/src/resources/testing.md +118 -0
- package/src/tools/check-environment.ts +319 -0
- package/src/tools/index.ts +58 -0
- package/src/tools/project-status.ts +183 -0
- package/src/tools/project-validate.ts +369 -0
- package/src/tools/schema-analyze.ts +410 -0
|
@@ -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,127 @@
|
|
|
1
|
+
# {PROJECT_NAME}
|
|
2
|
+
|
|
3
|
+
> {ONE_LINE_DESCRIPTION}
|
|
4
|
+
|
|
5
|
+
## Start Here
|
|
6
|
+
|
|
7
|
+
- `app/` — Expo Router pages (screens, navigation, API routes)
|
|
8
|
+
- `server/` — Lambda handlers (api.ts, worker.ts, image.ts)
|
|
9
|
+
- `db/` — Drizzle schema, migrations, seed data
|
|
10
|
+
- `lib/` — Shared code (auth context, API client)
|
|
11
|
+
- `sst.config.ts` — AWS infrastructure definition
|
|
12
|
+
|
|
13
|
+
## Structure
|
|
14
|
+
|
|
15
|
+
{ANNOTATED_DIRECTORY_TREE}
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm install # Install dependencies
|
|
21
|
+
pnpm dev # Start Expo dev server
|
|
22
|
+
pnpm test # Run all tests
|
|
23
|
+
npx drizzle-kit generate # Generate migration from schema changes
|
|
24
|
+
npx drizzle-kit migrate # Apply migrations locally
|
|
25
|
+
pnpm sst deploy --stage dev # Deploy to AWS
|
|
26
|
+
everystack update --channel production # OTA update (no redeploy)
|
|
27
|
+
everystack db:migrate # Run migrations on deployed Lambda
|
|
28
|
+
everystack db:seed # Seed database (dev only)
|
|
29
|
+
everystack db:psql --stage dev # Connect to database
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Key Principles
|
|
33
|
+
|
|
34
|
+
### Always TypeScript
|
|
35
|
+
|
|
36
|
+
Strict mode, no exceptions. Prefer `unknown` over `any`. Explicit return types on exported functions. Named exports only (no default exports).
|
|
37
|
+
|
|
38
|
+
### Always TDD
|
|
39
|
+
|
|
40
|
+
Every feature starts with a failing test.
|
|
41
|
+
|
|
42
|
+
1. Write a failing test that describes expected behavior
|
|
43
|
+
2. Implement the minimum code to make it pass
|
|
44
|
+
3. Refactor while keeping tests green
|
|
45
|
+
|
|
46
|
+
Tests live in `__tests__/` mirroring source structure, named `{feature}.test.ts`. Run from package dir: `npx jest __tests__/path/to/test.ts`
|
|
47
|
+
|
|
48
|
+
### Platform Security Over All Else
|
|
49
|
+
|
|
50
|
+
Three layers of defense. Each is a complete security boundary:
|
|
51
|
+
|
|
52
|
+
1. **Edge (CloudFront)** — JWT signature + expiry check. Invalid tokens never reach Lambda.
|
|
53
|
+
2. **Handler (Lambda)** — `SET LOCAL ROLE` via pgSettings, RPC role gates, rowOwnership, exposedTables.
|
|
54
|
+
3. **Database (PostgreSQL)** — GRANTs + RLS policies. The single source of truth for authorization.
|
|
55
|
+
|
|
56
|
+
Even if the handler has a bug, the database enforces access. Never skip RLS. Never connect as superuser in production.
|
|
57
|
+
|
|
58
|
+
### Progressive Complexity
|
|
59
|
+
|
|
60
|
+
Each tier builds on the previous. You never rip out what you have, you add to it.
|
|
61
|
+
|
|
62
|
+
- **V1:** Static site. Expo + S3 + CloudFront + Lambda SSR + OTA updates. No database.
|
|
63
|
+
- **V2:** Add PostgreSQL, PostgREST API, JWT auth, admin dashboard, logging.
|
|
64
|
+
- **V3:** Add SQS workers, Sharp image processing, S3 file storage.
|
|
65
|
+
|
|
66
|
+
### Web Standards
|
|
67
|
+
|
|
68
|
+
Handler uses `Request`/`Response` interface. No Express, no Fastify. Works with Expo Router API routes, Cloudflare Workers, Deno, Bun, or any Web Standard runtime.
|
|
69
|
+
|
|
70
|
+
### Schema-Agnostic
|
|
71
|
+
|
|
72
|
+
The library knows nothing about your tables. You pass your Drizzle schema to `createHandler()`. Your schema, your migrations, your database — the library provides the protocol.
|
|
73
|
+
|
|
74
|
+
## The Stack
|
|
75
|
+
|
|
76
|
+
**App:**
|
|
77
|
+
- TypeScript (strict mode)
|
|
78
|
+
- Expo + expo-router (file-based routing)
|
|
79
|
+
- React Native + react-native-web (cross-platform)
|
|
80
|
+
- @mgcrea/react-native-tailwind (Tailwind CSS styling)
|
|
81
|
+
|
|
82
|
+
**Data:**
|
|
83
|
+
- drizzle-orm + drizzle-kit (schema, queries, migrations)
|
|
84
|
+
- postgres (postgres.js driver)
|
|
85
|
+
- PostgreSQL (database)
|
|
86
|
+
|
|
87
|
+
**Infrastructure:**
|
|
88
|
+
- SST (infrastructure as code)
|
|
89
|
+
- AWS (S3, CloudFront, Lambda, RDS Aurora Serverless, SQS)
|
|
90
|
+
|
|
91
|
+
**Dev Tooling:**
|
|
92
|
+
- jest + ts-jest (testing)
|
|
93
|
+
- esbuild (Lambda bundling)
|
|
94
|
+
- pnpm (package management)
|
|
95
|
+
|
|
96
|
+
## Conventions
|
|
97
|
+
|
|
98
|
+
### Naming
|
|
99
|
+
|
|
100
|
+
- Package scope: `@everystack/*`
|
|
101
|
+
- Files: `kebab-case.ts`
|
|
102
|
+
- Exports: named (no default exports)
|
|
103
|
+
- Types: PascalCase, no `I` prefix
|
|
104
|
+
- Functions: camelCase
|
|
105
|
+
|
|
106
|
+
### Git
|
|
107
|
+
|
|
108
|
+
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `test:`, `refactor:`
|
|
109
|
+
- Scope optional: `feat(handler):`, `fix(client):`
|
|
110
|
+
- Tests must pass before commit
|
|
111
|
+
|
|
112
|
+
### Code
|
|
113
|
+
|
|
114
|
+
- TypeScript strict mode
|
|
115
|
+
- Prefer `unknown` over `any`
|
|
116
|
+
- Explicit return types on exported functions
|
|
117
|
+
- Prefer pure functions over classes
|
|
118
|
+
- Web Standard APIs (Request/Response)
|
|
119
|
+
- Error handling at boundaries only
|
|
120
|
+
|
|
121
|
+
### What NOT to Do
|
|
122
|
+
|
|
123
|
+
- Don't put schema or migrations in the library — they belong to the app
|
|
124
|
+
- Don't couple to Express/Fastify — use Request/Response
|
|
125
|
+
- Don't skip tests to ship faster
|
|
126
|
+
- Don't add features beyond what's tested
|
|
127
|
+
- Don't require a database for V1 — the stack works without one
|
|
@@ -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)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Device Security
|
|
2
|
+
|
|
3
|
+
> Device attestation and biometric auth. Import from `@everystack/security`.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
Read this when adding hardware-backed security: device trust verification, biometric authentication, or certificate pinning.
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
import { createSecurityHandler } from '@everystack/security';
|
|
12
|
+
|
|
13
|
+
const security = createSecurityHandler({
|
|
14
|
+
apple: { teamId: '...', bundleId: '...' },
|
|
15
|
+
google: { packageName: '...' },
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Device Attestation
|
|
20
|
+
|
|
21
|
+
Verify that requests come from genuine devices (not emulators or modified apps).
|
|
22
|
+
|
|
23
|
+
### Apple App Attest
|
|
24
|
+
```typescript
|
|
25
|
+
import { verifyAppleAttestation } from '@everystack/security';
|
|
26
|
+
|
|
27
|
+
const result = await verifyAppleAttestation({
|
|
28
|
+
attestation: base64AttestationData,
|
|
29
|
+
challenge: serverChallenge,
|
|
30
|
+
teamId: 'TEAM_ID',
|
|
31
|
+
bundleId: 'com.example.app',
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Google Play Integrity
|
|
36
|
+
```typescript
|
|
37
|
+
import { verifyPlayIntegrity } from '@everystack/security';
|
|
38
|
+
|
|
39
|
+
const result = await verifyPlayIntegrity({
|
|
40
|
+
token: integrityToken,
|
|
41
|
+
packageName: 'com.example.app',
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## RS256 Device Keys
|
|
46
|
+
|
|
47
|
+
Per-device RSA key pairs for request signing:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { generateDeviceKey, signRequest, verifySignature } from '@everystack/security/crypto';
|
|
51
|
+
|
|
52
|
+
// Client: generate and store key pair
|
|
53
|
+
const { publicKey, privateKey } = await generateDeviceKey();
|
|
54
|
+
|
|
55
|
+
// Client: sign requests
|
|
56
|
+
const signature = await signRequest(privateKey, requestBody);
|
|
57
|
+
|
|
58
|
+
// Server: verify signature
|
|
59
|
+
const valid = await verifySignature(publicKey, requestBody, signature);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Auth Plugin Integration
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { authPlugin } from '@everystack/auth/plugin';
|
|
66
|
+
|
|
67
|
+
authPlugin({
|
|
68
|
+
device: {
|
|
69
|
+
verify: async (attestation, claims) => {
|
|
70
|
+
// Verify device attestation
|
|
71
|
+
return true; // or false to reject
|
|
72
|
+
},
|
|
73
|
+
require: 'always', // or 'optional'
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Schema
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { securitySchema } from '@everystack/security/schema';
|
|
82
|
+
// Adds: device_attestations, device_keys tables
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Client SDK
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { createSecurityClient } from '@everystack/security/client';
|
|
89
|
+
|
|
90
|
+
const security = createSecurityClient({ baseUrl: '/api/security' });
|
|
91
|
+
await security.registerDevice({ publicKey, attestation });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Gotchas
|
|
95
|
+
|
|
96
|
+
- Apple App Attest requires iOS 14+ and a real device (not simulator)
|
|
97
|
+
- Google Play Integrity requires Google Play Services
|
|
98
|
+
- Device keys should be stored in the device's secure enclave/keystore
|
|
99
|
+
- Attestation verification should be done server-side only
|