@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.
Files changed (40) hide show
  1. package/README.md +100 -0
  2. package/package.json +39 -0
  3. package/src/index.ts +58 -0
  4. package/src/prompts/add-feature.ts +163 -0
  5. package/src/prompts/debug.ts +136 -0
  6. package/src/prompts/deploy.ts +131 -0
  7. package/src/prompts/design-schema.ts +104 -0
  8. package/src/prompts/index.ts +16 -0
  9. package/src/prompts/new-app.ts +211 -0
  10. package/src/prompts/secure.ts +231 -0
  11. package/src/resources/adding-database.md +169 -0
  12. package/src/resources/admin.md +81 -0
  13. package/src/resources/auth.md +115 -0
  14. package/src/resources/aws-setup.md +173 -0
  15. package/src/resources/cli.md +108 -0
  16. package/src/resources/client-api.md +145 -0
  17. package/src/resources/core.md +196 -0
  18. package/src/resources/deployment.md +146 -0
  19. package/src/resources/events.md +87 -0
  20. package/src/resources/first-run.md +100 -0
  21. package/src/resources/getting-started.md +75 -0
  22. package/src/resources/handler-options.md +114 -0
  23. package/src/resources/images.md +73 -0
  24. package/src/resources/index.ts +224 -0
  25. package/src/resources/jobs.md +97 -0
  26. package/src/resources/logging.md +91 -0
  27. package/src/resources/plugins.md +68 -0
  28. package/src/resources/project-claude-md.md +127 -0
  29. package/src/resources/query-protocol.md +129 -0
  30. package/src/resources/schema-patterns.md +167 -0
  31. package/src/resources/security-device.md +99 -0
  32. package/src/resources/security.md +270 -0
  33. package/src/resources/ssr.md +82 -0
  34. package/src/resources/storage.md +63 -0
  35. package/src/resources/testing.md +118 -0
  36. package/src/tools/check-environment.ts +319 -0
  37. package/src/tools/index.ts +58 -0
  38. package/src/tools/project-status.ts +183 -0
  39. package/src/tools/project-validate.ts +369 -0
  40. 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