@everystack/mcp 0.3.3 → 0.4.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/dist/adding-database.md +36 -23
- package/dist/cli.md +44 -3
- package/dist/core.md +19 -1
- package/dist/database-operations.md +236 -0
- package/dist/derived-objects.md +225 -0
- package/dist/index.cjs +112 -102
- package/dist/project-claude-md.md +15 -12
- package/dist/schema-patterns.md +92 -106
- package/package.json +3 -3
- package/src/gates/detectors/hand-written-migration.ts +12 -5
- package/src/index.ts +1 -1
- package/src/prompts/add-feature.ts +4 -4
- package/src/prompts/debug.ts +3 -4
- package/src/prompts/deploy.ts +17 -8
- package/src/prompts/design-schema.ts +45 -59
- package/src/prompts/new-app.ts +21 -19
- package/src/prompts/secure.ts +3 -3
- package/src/resources/adding-database.md +36 -23
- package/src/resources/cli.md +44 -3
- package/src/resources/core.md +19 -1
- package/src/resources/database-operations.md +236 -0
- package/src/resources/derived-objects.md +225 -0
- package/src/resources/index.ts +15 -1
- package/src/resources/project-claude-md.md +15 -12
- package/src/resources/schema-patterns.md +92 -106
package/dist/schema-patterns.md
CHANGED
|
@@ -1,167 +1,153 @@
|
|
|
1
1
|
# Schema Design Patterns
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Declaring TABLES with `defineModel` in `db/models/` — fields, authz, relations, indexes.
|
|
4
4
|
|
|
5
5
|
## When to Use
|
|
6
|
-
Read this when designing your database schema or adding tables to an existing app.
|
|
6
|
+
Read this when designing your database schema or adding tables to an existing app. This
|
|
7
|
+
resource covers **tables** (`defineModel`). For the derived layer —
|
|
8
|
+
views, materialized views, functions, triggers, standalone sequences — read
|
|
9
|
+
`everystack://derived-objects` (they deploy via `db:reconcile`, not migrations). For the
|
|
10
|
+
operational verbs that move a database (`db:sync`, `db:generate`, `db:plan`/`db:apply`,
|
|
11
|
+
`db:check`), read `everystack://database-operations`.
|
|
7
12
|
|
|
8
13
|
## Schema Location
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
The whole declared database lives in `db/models/` — one `defineModel` per table, composed
|
|
16
|
+
onto a `defineModule`. This is the single source of truth for the handler, SSR, RLS, and
|
|
17
|
+
grants. You do **not** hand-write `db/schema.ts`, and you do **not** run
|
|
18
|
+
`drizzle-kit generate`/`drizzle-kit migrate` — the Model is authored, and everystack
|
|
19
|
+
generates the migration (`db:generate`) and the typed artifact (`db/schema.generated.ts`,
|
|
20
|
+
never edited). Never hand-write a SQL migration.
|
|
11
21
|
|
|
12
22
|
## Basic Table
|
|
13
23
|
|
|
14
24
|
```typescript
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
export const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
import { defineModel, field, can } from '@everystack/model';
|
|
26
|
+
|
|
27
|
+
export const Post = defineModel('posts', {
|
|
28
|
+
fields: {
|
|
29
|
+
id: field.uuid().primaryKey().defaultRandom(),
|
|
30
|
+
body: field.text().notNull(),
|
|
31
|
+
authorId: field.uuid().notNull().references('users', 'id'),
|
|
32
|
+
status: field.text().notNull().default('draft'),
|
|
33
|
+
createdAt: field.timestamptz().notNull().defaultNow(),
|
|
34
|
+
updatedAt: field.timestamptz().notNull().defaultNow(),
|
|
35
|
+
deletedAt: field.timestamptz(),
|
|
36
|
+
},
|
|
37
|
+
abilities: [can('read'), can('create'), can('update'), can('delete')],
|
|
25
38
|
});
|
|
26
39
|
```
|
|
27
40
|
|
|
41
|
+
Column names are `snake_case` in the database; the field key is the app-side name — the
|
|
42
|
+
generator maps `authorId` → `author_id` for you.
|
|
43
|
+
|
|
28
44
|
## Common Patterns
|
|
29
45
|
|
|
30
46
|
### UUID vs Serial Primary Keys
|
|
31
47
|
Prefer UUID for user-facing IDs (prevents enumeration attacks). Use serial for internal-only tables.
|
|
32
48
|
|
|
33
49
|
```typescript
|
|
34
|
-
id: uuid(
|
|
35
|
-
id: serial(
|
|
50
|
+
id: field.uuid().primaryKey().defaultRandom(), // Preferred for API-exposed tables
|
|
51
|
+
id: field.serial().primaryKey(), // OK for internal tables
|
|
36
52
|
```
|
|
37
53
|
|
|
38
54
|
### Timestamps
|
|
39
55
|
Always include `createdAt` and `updatedAt`:
|
|
40
56
|
```typescript
|
|
41
|
-
createdAt:
|
|
42
|
-
updatedAt:
|
|
57
|
+
createdAt: field.timestamptz().notNull().defaultNow(),
|
|
58
|
+
updatedAt: field.timestamptz().notNull().defaultNow(),
|
|
43
59
|
```
|
|
44
60
|
|
|
45
61
|
### Soft Delete
|
|
46
62
|
Add `deletedAt` for soft-deletable tables:
|
|
47
63
|
```typescript
|
|
48
|
-
deletedAt:
|
|
64
|
+
deletedAt: field.timestamptz(),
|
|
49
65
|
```
|
|
50
66
|
Configure in handler: `softDelete: { column: 'deletedAt', tables: ['posts'] }`.
|
|
51
67
|
|
|
52
68
|
### Foreign Keys
|
|
53
69
|
```typescript
|
|
54
|
-
authorId: uuid(
|
|
70
|
+
authorId: field.uuid().notNull().references('users', 'id'),
|
|
55
71
|
```
|
|
56
72
|
|
|
57
73
|
### Enums
|
|
58
74
|
```typescript
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
export const roleEnum = pgEnum('role', ['user', 'admin']);
|
|
62
|
-
|
|
63
|
-
export const users = pgTable('users', {
|
|
64
|
-
role: roleEnum('role').default('user').notNull(),
|
|
65
|
-
});
|
|
75
|
+
role: field.enum('role', ['user', 'admin']).notNull().default('user'),
|
|
66
76
|
```
|
|
67
77
|
|
|
68
|
-
## Relations
|
|
78
|
+
## Relations
|
|
69
79
|
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
Declare relations on the Model — the generator emits both the Drizzle relations (used by
|
|
81
|
+
the relational query API, `db.query.posts.findMany({ with: { author: true } })`) and the
|
|
82
|
+
handler's embedding config, so they can't drift apart.
|
|
72
83
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
```typescript
|
|
85
|
+
export const Post = defineModel('posts', {
|
|
86
|
+
fields: { /* … */ },
|
|
87
|
+
relations: {
|
|
88
|
+
author: { model: 'users', from: 'authorId', to: 'id' },
|
|
89
|
+
comments: { model: 'comments', from: 'id', to: 'postId', many: true },
|
|
90
|
+
},
|
|
91
|
+
abilities: [can('read')],
|
|
92
|
+
});
|
|
77
93
|
```
|
|
78
94
|
|
|
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
95
|
## Indexes
|
|
82
96
|
|
|
83
97
|
```typescript
|
|
84
|
-
import {
|
|
85
|
-
|
|
86
|
-
export const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
]
|
|
98
|
+
import { defineModel, field, can, index } from '@everystack/model';
|
|
99
|
+
|
|
100
|
+
export const Post = defineModel('posts', {
|
|
101
|
+
fields: { /* … */ },
|
|
102
|
+
indexes: [
|
|
103
|
+
index(['authorId']),
|
|
104
|
+
index(['slug']).unique(),
|
|
105
|
+
],
|
|
106
|
+
abilities: [can('read')],
|
|
107
|
+
});
|
|
92
108
|
```
|
|
93
109
|
|
|
94
|
-
##
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
# Generate migration from schema changes
|
|
98
|
-
npx drizzle-kit generate
|
|
99
|
-
|
|
100
|
-
# Apply migrations locally
|
|
101
|
-
npx drizzle-kit migrate
|
|
110
|
+
## Authorization is declared, not migrated
|
|
102
111
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
You never hand-write `CREATE ROLE`, `GRANT`, or `CREATE POLICY`. Abilities on the Model
|
|
113
|
+
compile to grants + RLS, and `db:sync`/`db:apply` deploy them. `can('read')` grants `anon`
|
|
114
|
+
+ `authenticated`; `can('read', { role: 'admin' })` narrows to a role. RLS is required —
|
|
115
|
+
the CI gate (`db:check`) refuses a table without it. See `everystack://security` for the
|
|
116
|
+
policy patterns the abilities compile to.
|
|
106
117
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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)
|
|
118
|
+
```typescript
|
|
119
|
+
export const Post = defineModel('posts', {
|
|
120
|
+
fields: { /* … */ },
|
|
121
|
+
abilities: [
|
|
122
|
+
can('read'), // anon + authenticated
|
|
123
|
+
can('create', { role: 'authenticated' }),
|
|
124
|
+
can('update', { role: 'authenticated', own: 'authorId' }), // row ownership
|
|
125
|
+
can('delete', { role: 'admin' }),
|
|
126
|
+
],
|
|
127
|
+
});
|
|
145
128
|
```
|
|
146
129
|
|
|
147
130
|
## Handler Config for Schema
|
|
148
131
|
|
|
132
|
+
The handler config is **derived** from the Models (`deriveHandlerConfig(models)`), so
|
|
133
|
+
`exposedTables`, `hiddenColumns`, `protectedFields`, and `relations` come from the
|
|
134
|
+
declarations rather than a hand-kept parallel list:
|
|
135
|
+
|
|
149
136
|
```typescript
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
relations: {
|
|
155
|
-
posts: { author: { table: 'users', from: 'authorId', to: 'id' } },
|
|
156
|
-
},
|
|
157
|
-
});
|
|
137
|
+
import { deriveHandlerConfig } from '@everystack/model';
|
|
138
|
+
import { models } from '../db/models';
|
|
139
|
+
|
|
140
|
+
createHandler(db, schema, deriveHandlerConfig(models));
|
|
158
141
|
```
|
|
159
142
|
|
|
160
143
|
## Gotchas
|
|
161
144
|
|
|
162
|
-
-
|
|
163
|
-
|
|
164
|
-
-
|
|
165
|
-
|
|
166
|
-
- RLS
|
|
167
|
-
|
|
145
|
+
- Field keys are app-side names (camelCase); the database column is `snake_case` — the
|
|
146
|
+
generator maps `authorId` → `author_id`.
|
|
147
|
+
- Edit the Model, then move the database with `db:sync` (dev) or `db:generate` +
|
|
148
|
+
`db:plan`/`db:apply` (protected stages). Never run `drizzle-kit generate`.
|
|
149
|
+
- RLS and grants are declared with `can()`, never hand-written; `db:check` fails a table
|
|
150
|
+
without RLS.
|
|
151
|
+
- `db/schema.generated.ts` is a generated artifact — never edit it; `db:check` refuses
|
|
152
|
+
drift.
|
|
153
|
+
- Connect as `authenticator` in production (never RDS master/superuser).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Governance layer that governs how any agent builds everystack — grounding, cheat gates, and Model-aware tooling over MCP",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "Scalable Technology, Inc. <licensing@scalable.technology>",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"tsx": "4.21.0",
|
|
41
41
|
"typescript": "5.9.3",
|
|
42
42
|
"zod": "3.25.67",
|
|
43
|
-
"@everystack/cli": "0.
|
|
44
|
-
"@everystack/model": "0.
|
|
43
|
+
"@everystack/cli": "0.4.2",
|
|
44
|
+
"@everystack/model": "0.4.1"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"test": "jest",
|
|
@@ -11,13 +11,17 @@
|
|
|
11
11
|
*
|
|
12
12
|
* The homes that ARE legitimately authored — this gate deliberately matches
|
|
13
13
|
* none of them:
|
|
14
|
-
* - `db/models/` — the Models
|
|
15
|
-
*
|
|
16
|
-
* via `db:reconcile`/`db:sync`.
|
|
14
|
+
* - `db/models/` — the Models AND the derived-layer descriptors (defineView /
|
|
15
|
+
* defineMaterializedView / defineFunction / defineSql / trigger()) — the
|
|
16
|
+
* whole declared database, deployed via `db:reconcile`/`db:sync`.
|
|
17
17
|
* - `db/backfills/` — one-shot DATA jobs, run via `db:backfill` (content-
|
|
18
18
|
* addressed, own record, never a schema side effect).
|
|
19
19
|
* - `db/schema.ts` — the thin hand-maintained barrel that re-exports the
|
|
20
20
|
* generated schema plus package-owned schemas.
|
|
21
|
+
*
|
|
22
|
+
* `db/sql/` is RETIRED (B7, read-model-everywhere): raw-SQL derived objects
|
|
23
|
+
* moved into descriptors. A write there is flagged — the CLI verbs fail on the
|
|
24
|
+
* directory too; this gate just says so before the file lands.
|
|
21
25
|
*/
|
|
22
26
|
|
|
23
27
|
import { relative } from 'path';
|
|
@@ -27,6 +31,8 @@ import type { CheatGate, ToolCallContext } from '../types.js';
|
|
|
27
31
|
const MIGRATION_SQL = /(?:^|\/)(?:drizzle|migrations)\/[^/]+\.sql$/i;
|
|
28
32
|
/** The generated drizzle schema artifact (any location — monorepos relocate it via --schema-out). */
|
|
29
33
|
const GENERATED_SCHEMA = /schema\.generated\.tsx?$/;
|
|
34
|
+
/** The retired raw-SQL derived home — descriptors in db/models are the single home now. */
|
|
35
|
+
const RETIRED_DB_SQL = /(?:^|\/)db\/sql\/[^/]+\.sql$/i;
|
|
30
36
|
|
|
31
37
|
function rel(ctx: ToolCallContext, p: string): string {
|
|
32
38
|
try {
|
|
@@ -41,8 +47,8 @@ export const handWrittenMigration: CheatGate = {
|
|
|
41
47
|
tier: 'framework',
|
|
42
48
|
severity: 'deny',
|
|
43
49
|
guide:
|
|
44
|
-
'Nobody authors migrations. Schema changes have
|
|
45
|
-
conform: 'edit db/models/ then `everystack db:sync` (dev) or `db:plan` → `db:apply` (protected) · db/
|
|
50
|
+
'Nobody authors migrations. Schema changes have two homes: the WHOLE declared database — tables/constraints/authz (defineModel) AND functions/views/matviews (defineView/defineMaterializedView/defineFunction/defineSql descriptors) — lives in db/models/ (edit the declaration, then `everystack db:sync` on dev — protected stages take `db:plan` → `db:apply`); one-shot DATA moves are authored in db/backfills/*.sql (run via db:backfill, never as a schema side effect). db/sql/ is retired — raw-SQL derived objects moved into descriptors. No migration file, ever — and generated artifacts (schema.generated.ts) are never edited.',
|
|
51
|
+
conform: 'edit db/models/ (Models + derived descriptors) then `everystack db:sync` (dev) or `db:plan` → `db:apply` (protected) · db/backfills/ for data moves',
|
|
46
52
|
verify: 'everystack db:check passes (declared state composes; generated artifacts match regeneration) AND everystack db:fingerprint reports MATCH',
|
|
47
53
|
detect(ctx: ToolCallContext): string | null {
|
|
48
54
|
if (ctx.tool !== 'Write' && ctx.tool !== 'Edit') return null;
|
|
@@ -50,6 +56,7 @@ export const handWrittenMigration: CheatGate = {
|
|
|
50
56
|
if (!p) return null;
|
|
51
57
|
if (MIGRATION_SQL.test(p)) return `${rel(ctx, p)} is a SQL migration being written by hand`;
|
|
52
58
|
if (GENERATED_SCHEMA.test(p)) return `${rel(ctx, p)} is a generated artifact (compiled from your Models — db:check refuses hand edits)`;
|
|
59
|
+
if (RETIRED_DB_SQL.test(p)) return `${rel(ctx, p)} is in the retired db/sql home — declare it as a descriptor (defineView/defineMaterializedView/defineFunction/defineSql) in db/models/ instead; the CLI verbs fail on this directory`;
|
|
53
60
|
return null;
|
|
54
61
|
},
|
|
55
62
|
};
|
package/src/index.ts
CHANGED
|
@@ -71,7 +71,7 @@ const INSTRUCTIONS = [
|
|
|
71
71
|
'1. Read everystack://core for architecture and conventions.',
|
|
72
72
|
'2. Read everystack://security before any deployment or auth guidance.',
|
|
73
73
|
'3. Load detail resources on demand when the user asks about specific features.',
|
|
74
|
-
'4. Data lives in PostgreSQL via Models/Modules and is served through the API — never bundle large computed data into the app. NOBODY AUTHORS MIGRATIONS. Schema work has
|
|
74
|
+
'4. Data lives in PostgreSQL via Models/Modules and is served through the API — never bundle large computed data into the app. NOBODY AUTHORS MIGRATIONS. Schema work has two homes: the WHOLE declared database — tables/constraints/authz (defineModel) AND functions/views/matviews (defineView/defineMaterializedView/defineFunction/defineSql descriptors) — is DECLARED in `db/models/` (edit the declaration, then `everystack db:sync` moves the dev database to the checkout — verified by fingerprint; the derived layer deploys via `db:reconcile`/`db:sync`); one-shot DATA moves are authored in `db/backfills/*.sql` (run via `db:backfill`, never as a schema side effect). Protected stages never take a sync: `db:plan` mints a reviewable, fingerprint-pinned edge and `db:apply` verifies at both ends — the checkout must descend from the commit declaring the target\'s state ("rebase first"), and destructive plans are confirmed, snapshotted, and approver-gated (`db:approvers`). CI runs `everystack db:check` (the merged declared state must compose; generated artifacts must match regeneration byte-for-byte). Per-branch dev databases: `db:template:refresh` + `db:branch`; deployed feature stages fork data with `db:fork`. Reuse `@everystack/ui` components; never put secret values behind `EXPO_PUBLIC_*`.',
|
|
75
75
|
'5. When the user wants to start a new project, run check_environment (phase "local" for dev, "deploy" for deployment) to verify prerequisites.',
|
|
76
76
|
'6. When the user needs to interact with deployed infrastructure, guide them to use the everystack CLI.',
|
|
77
77
|
].join('\n');
|
|
@@ -105,7 +105,7 @@ const FEATURES: Record<string, { resource: string; packages: string[]; steps: st
|
|
|
105
105
|
packages: [],
|
|
106
106
|
steps: [
|
|
107
107
|
'Events use PostgreSQL LISTEN/NOTIFY — built into the database',
|
|
108
|
-
'
|
|
108
|
+
'Declare the NOTIFY trigger the descriptor way (everystack://derived-objects): a defineFunction returning trigger, wired via trigger() on the Model — never a hand-written SQL trigger or a db/sql file',
|
|
109
109
|
'Set up a listener Lambda with persistent database connection',
|
|
110
110
|
'Add WebSocket fan-out for client delivery',
|
|
111
111
|
'Use the useSignal hook in React components for real-time updates',
|
|
@@ -139,8 +139,8 @@ export function registerAddFeaturePrompt(server: McpServer): void {
|
|
|
139
139
|
`1. Read the ${f.resource} resource for full documentation.`,
|
|
140
140
|
'2. Read everystack://core for architecture context.',
|
|
141
141
|
feature === 'auth' || feature === 'security' ? '3. Read everystack://security for the security model.' : '',
|
|
142
|
-
projectPath ? `4.
|
|
143
|
-
projectPath ?
|
|
142
|
+
projectPath ? `4. Read the project's CLAUDE.md and structure under "${projectPath}" to understand the current state.` : '',
|
|
143
|
+
projectPath ? '5. After changes: `pnpm test`, and if the schema changed, `everystack db:check`.' : '',
|
|
144
144
|
'',
|
|
145
145
|
'## Steps',
|
|
146
146
|
'',
|
|
@@ -151,7 +151,7 @@ export function registerAddFeaturePrompt(server: McpServer): void {
|
|
|
151
151
|
'## After Setup',
|
|
152
152
|
'',
|
|
153
153
|
'- Run tests to verify the integration',
|
|
154
|
-
'-
|
|
154
|
+
'- If the schema changed, run `everystack db:check`',
|
|
155
155
|
'- Read the relevant resource docs for advanced configuration',
|
|
156
156
|
].filter(Boolean).join('\n'),
|
|
157
157
|
},
|
package/src/prompts/debug.ts
CHANGED
|
@@ -22,9 +22,8 @@ export function registerDebugPrompt(server: McpServer): void {
|
|
|
22
22
|
'## Instructions',
|
|
23
23
|
'',
|
|
24
24
|
'1. Read everystack://core for architecture context.',
|
|
25
|
-
projectPath ? `2.
|
|
26
|
-
projectPath ?
|
|
27
|
-
projectPath ? `4. Run schema_analyze with projectPath="${projectPath}" to check schema/handler alignment.` : '',
|
|
25
|
+
projectPath ? `2. Read the project's CLAUDE.md and \`db/models/\` under "${projectPath}" to understand its schema and conventions.` : '',
|
|
26
|
+
projectPath ? '3. If the issue is schema/database-related, run `everystack db:check` (declared state composes) and `everystack db:fingerprint` (live schema matches the Models) to localize drift.' : '',
|
|
28
27
|
'',
|
|
29
28
|
'## Diagnostic Framework',
|
|
30
29
|
'',
|
|
@@ -125,7 +124,7 @@ export function registerDebugPrompt(server: McpServer): void {
|
|
|
125
124
|
'1. Identify the root cause',
|
|
126
125
|
'2. Propose a fix with the specific code change',
|
|
127
126
|
'3. Verify the fix resolves the issue',
|
|
128
|
-
'4. Run
|
|
127
|
+
'4. Run `pnpm test` and (for schema changes) `everystack db:check` to ensure no new issues were introduced',
|
|
129
128
|
].filter(Boolean).join('\n'),
|
|
130
129
|
},
|
|
131
130
|
},
|
package/src/prompts/deploy.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
export function registerDeployPrompt(server: McpServer): void {
|
|
5
5
|
server.prompt(
|
|
6
6
|
'deploy',
|
|
7
|
-
'Step-by-step deployment walkthrough for a specific stage. Covers AWS credentials, SST deploy,
|
|
7
|
+
'Step-by-step deployment walkthrough for a specific stage. Covers AWS credentials, SST deploy, the SAFE schema flow (db:plan → db:apply, snapshot-first), and verification.',
|
|
8
8
|
{
|
|
9
9
|
stage: z.enum(['dev', 'staging', 'production']).describe('Deployment stage'),
|
|
10
10
|
projectPath: z.string().optional().describe('Absolute path to project root'),
|
|
@@ -25,8 +25,8 @@ export function registerDeployPrompt(server: McpServer): void {
|
|
|
25
25
|
'',
|
|
26
26
|
'1. Read everystack://deployment for infrastructure setup.',
|
|
27
27
|
'2. Read everystack://security for AWS credential setup and deployment checklist.',
|
|
28
|
-
|
|
29
|
-
projectPath ? `4. Run
|
|
28
|
+
'3. Read everystack://database-operations for the SAFE schema-migration flow (db:plan → db:apply, snapshot-first) — a protected stage is NOT migrated with db:migrate.',
|
|
29
|
+
projectPath ? `4. Run check_environment with projectPath="${projectPath}" to confirm prerequisites (Node, SST, AWS credentials) before deploying.` : '',
|
|
30
30
|
'',
|
|
31
31
|
'## Pre-Deploy Checklist',
|
|
32
32
|
'',
|
|
@@ -49,8 +49,8 @@ export function registerDeployPrompt(server: McpServer): void {
|
|
|
49
49
|
'### Production Safety',
|
|
50
50
|
'',
|
|
51
51
|
'- [ ] All tests pass: `pnpm test`',
|
|
52
|
-
'- [ ]
|
|
53
|
-
'- [ ] RLS
|
|
52
|
+
'- [ ] `everystack db:check` passes (declared state composes; generated artifacts match)',
|
|
53
|
+
'- [ ] RLS is declared via `can()` abilities on every Model (compiles to policies + grants)',
|
|
54
54
|
'- [ ] pgSettings is configured in handler',
|
|
55
55
|
'- [ ] No hardcoded secrets in source',
|
|
56
56
|
'- [ ] .env files are not committed',
|
|
@@ -71,10 +71,19 @@ export function registerDeployPrompt(server: McpServer): void {
|
|
|
71
71
|
'- SQS queues (V3)',
|
|
72
72
|
'- IAM roles and policies',
|
|
73
73
|
'',
|
|
74
|
-
'###
|
|
74
|
+
'### 2. Database Schema (V2+) — the SAFE flow',
|
|
75
|
+
'',
|
|
76
|
+
'A protected stage is migrated by minting a reviewable plan and applying it credential-free',
|
|
77
|
+
'(the operator never holds the database URL). See everystack://database-operations.',
|
|
75
78
|
'```bash',
|
|
76
|
-
`everystack db:
|
|
77
|
-
|
|
79
|
+
`everystack db:check # gate: declared state composes`,
|
|
80
|
+
`everystack db:plan --stage ${stage} --out ${stage}.plan.json # mint the edge (read-only) — REVIEW it`,
|
|
81
|
+
isProduction ? `everystack db:snapshot --stage ${stage} # physical RDS snapshot first (instant rollback)` : '',
|
|
82
|
+
`everystack db:apply --plan ${stage}.plan.json --stage ${stage} # verify → apply → verify`,
|
|
83
|
+
`# destructive plans (drops / narrowing types) also need: --confirm + a snapshot + the`,
|
|
84
|
+
`# stage's approver set (everystack db:approvers --stage ${stage} --set "…")`,
|
|
85
|
+
`everystack db:reconcile --check --stage ${stage} # confirm the compute layer (views/matviews/functions) matches`,
|
|
86
|
+
stage === 'dev' ? `everystack db:seed --stage ${stage} # dev only` : '',
|
|
78
87
|
'```',
|
|
79
88
|
'',
|
|
80
89
|
'### 4. Verify',
|
|
@@ -4,10 +4,10 @@ import { z } from 'zod';
|
|
|
4
4
|
export function registerDesignSchemaPrompt(server: McpServer): void {
|
|
5
5
|
server.prompt(
|
|
6
6
|
'design-schema',
|
|
7
|
-
'Interactive schema design: plain English description →
|
|
7
|
+
'Interactive schema design (0.4.0): plain English description → declared Models (defineModel + can abilities) + derived descriptors + testing checklist. Authz and handler config are DERIVED from the Models, never hand-written.',
|
|
8
8
|
{
|
|
9
9
|
description: z.string().describe('Plain English description of the data model (e.g., "blog with posts, comments, and tags")'),
|
|
10
|
-
projectPath: z.string().optional().describe('Absolute path to project root (to
|
|
10
|
+
projectPath: z.string().optional().describe('Absolute path to project root (to read the existing db/models/)'),
|
|
11
11
|
},
|
|
12
12
|
async ({ description, projectPath }) => {
|
|
13
13
|
return {
|
|
@@ -19,81 +19,67 @@ export function registerDesignSchemaPrompt(server: McpServer): void {
|
|
|
19
19
|
text: [
|
|
20
20
|
`Design a database schema for: ${description}`,
|
|
21
21
|
'',
|
|
22
|
+
'IMPORTANT (0.4.0): you declare the schema, you do NOT hand-write it. Tables are',
|
|
23
|
+
'`defineModel` in `db/models/`; authorization is `can()` abilities on the Model that',
|
|
24
|
+
'COMPILE to RLS + grants; the handler config is DERIVED from the Models. Never hand-write',
|
|
25
|
+
'`db/schema.ts`, a SQL migration, `CREATE POLICY`, or `GRANT` — those are generated.',
|
|
26
|
+
'',
|
|
22
27
|
'## Instructions',
|
|
23
28
|
'',
|
|
24
|
-
'1. Read everystack://schema-patterns for
|
|
25
|
-
'2. Read everystack://
|
|
26
|
-
'3. Read everystack://
|
|
27
|
-
|
|
29
|
+
'1. Read everystack://schema-patterns for how to declare TABLES (defineModel, field, can, relations).',
|
|
30
|
+
'2. Read everystack://derived-objects IF the model needs views, materialized views, functions, or triggers.',
|
|
31
|
+
'3. Read everystack://security for the authorization model (how can() abilities become RLS + grants).',
|
|
32
|
+
'4. Read everystack://database-operations for how the declaration reaches a database (db:sync on dev).',
|
|
33
|
+
projectPath ? `5. Read the existing \`db/models/\` under "${projectPath}" to match its conventions before adding to it.` : '',
|
|
28
34
|
'',
|
|
29
35
|
'## Deliverables',
|
|
30
36
|
'',
|
|
31
37
|
'Generate ALL of the following:',
|
|
32
38
|
'',
|
|
33
|
-
'### 1.
|
|
34
|
-
'',
|
|
35
|
-
'Follow these conventions:',
|
|
36
|
-
'- UUID primary keys: `uuid(\'id\').primaryKey().defaultRandom()`',
|
|
37
|
-
'- Timestamps: `timestamp(\'created_at\', { withTimezone: true }).defaultNow().notNull()`',
|
|
38
|
-
'- Soft delete columns: `deletedAt` + `deletedBy` on user-facing tables',
|
|
39
|
-
'- Foreign keys with `.references(() => table.column)`',
|
|
40
|
-
'- Export all tables as named exports',
|
|
41
|
-
'- Add Drizzle `relations()` for SSR query building and API embedding',
|
|
42
|
-
'',
|
|
43
|
-
'### 2. SQL Migration with RLS',
|
|
44
|
-
'',
|
|
45
|
-
'Generate a custom SQL migration that includes:',
|
|
39
|
+
'### 1. Models (db/models/<table>.ts — one file per table)',
|
|
46
40
|
'',
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
41
|
+
'Declare each table with `defineModel`, following these conventions:',
|
|
42
|
+
'- UUID primary keys: `id: field.uuid().primaryKey().defaultRandom()`',
|
|
43
|
+
'- Timestamps: `createdAt: field.timestamptz().defaultNow().notNull()`',
|
|
44
|
+
'- Soft delete: a `deletedAt: field.timestamptz()` field opts the table into soft-delete',
|
|
45
|
+
'- Foreign keys via relations: `field.uuid().references(() => Author)` / `belongsTo`/`hasMany`',
|
|
46
|
+
'- Sensitive columns: `.private()` (hidden from the API); write-guarded: `.readonly()`',
|
|
47
|
+
'- Named exports (PascalCase model var, e.g. `export const Post = defineModel(\'posts\', …)`)',
|
|
50
48
|
'',
|
|
51
|
-
'
|
|
52
|
-
'GRANT SELECT ON table_name TO anon;',
|
|
53
|
-
'GRANT SELECT, INSERT, UPDATE, DELETE ON table_name TO authenticated;',
|
|
54
|
-
'GRANT ALL ON table_name TO admin;',
|
|
49
|
+
'### 2. Authorization — `can()` abilities ON each Model (NOT hand-written RLS)',
|
|
55
50
|
'',
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'CREATE POLICY "anon_select" ON table_name FOR SELECT TO anon USING (deleted_at IS NULL);',
|
|
51
|
+
'Declare the access pattern as abilities; the compiler emits the RLS policies and GRANTs.',
|
|
52
|
+
'Every table MUST make its read decision or `db:check` fails it:',
|
|
59
53
|
'',
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
|
|
63
|
-
'',
|
|
64
|
-
'
|
|
65
|
-
'
|
|
54
|
+
'```ts',
|
|
55
|
+
'abilities: [',
|
|
56
|
+
" can('read'), // public read (anon + authenticated)",
|
|
57
|
+
" can('read', { owner: 'authorId' }), // rows the caller owns",
|
|
58
|
+
" can('manage', { owner: 'authorId' }), // owner can write their own rows",
|
|
59
|
+
" can('read', { role: 'admin' }), // a specific role only",
|
|
60
|
+
'],',
|
|
66
61
|
'```',
|
|
67
62
|
'',
|
|
68
|
-
'
|
|
63
|
+
'A private/operational table declares `private: true` (generated, but not a generic-API',
|
|
64
|
+
'resource). Row ownership, soft-delete, hidden/protected columns, and relations are all',
|
|
65
|
+
'DERIVED from the Model into the handler config — do not write a createHandler options blob',
|
|
66
|
+
'by hand.',
|
|
69
67
|
'',
|
|
70
|
-
'### 3.
|
|
68
|
+
'### 3. Derived objects (only if the model needs compute)',
|
|
71
69
|
'',
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'- `rowOwnership`: for user-scoped tables',
|
|
76
|
-
'- `softDelete`: for tables with deletedAt columns',
|
|
77
|
-
'- `protectedFields`: fields users cannot set directly (role, deletedAt)',
|
|
78
|
-
'- `hiddenColumns`: sensitive columns not returned in API responses',
|
|
70
|
+
'If the design needs a view, materialized view, function, or trigger, declare it as a',
|
|
71
|
+
'descriptor in `db/models/derived.ts` per everystack://derived-objects — NOT as a SQL',
|
|
72
|
+
'migration. Wire them on `defineModule({ models, derived })`.',
|
|
79
73
|
'',
|
|
80
|
-
'### 4. Testing
|
|
74
|
+
'### 4. Testing + apply checklist',
|
|
81
75
|
'',
|
|
82
|
-
'
|
|
83
|
-
'- [ ] Anonymous users can read
|
|
84
|
-
'- [ ]
|
|
85
|
-
'- [ ] Authenticated users can only read/write their own data',
|
|
76
|
+
'- [ ] Every table declares a read ability or `private: true` (db:check gate)',
|
|
77
|
+
'- [ ] Anonymous users can read only what `can(\'read\')` (no owner/role) allows',
|
|
78
|
+
'- [ ] Owner-scoped rows are invisible and unwritable across users (IDOR)',
|
|
86
79
|
'- [ ] Soft-deleted rows are hidden from normal queries',
|
|
87
|
-
'- [ ]
|
|
88
|
-
'- [ ]
|
|
89
|
-
'- [ ]
|
|
90
|
-
'',
|
|
91
|
-
'Test each RLS policy with:',
|
|
92
|
-
'```sql',
|
|
93
|
-
'SET LOCAL ROLE authenticated;',
|
|
94
|
-
"SELECT set_config('request.jwt.claims', '{\"sub\": \"user-uuid\", \"role\": \"authenticated\"}', true);",
|
|
95
|
-
'SELECT * FROM table_name; -- should only return own rows',
|
|
96
|
-
'```',
|
|
80
|
+
'- [ ] `everystack db:sync --database-url "$DATABASE_URL"` applies cleanly on a dev DB',
|
|
81
|
+
'- [ ] `everystack db:check` passes (declared state composes; generated artifacts match)',
|
|
82
|
+
'- [ ] `everystack db:authz:test --stage dev` proves the compiled RLS enforces the abilities',
|
|
97
83
|
].filter(Boolean).join('\n'),
|
|
98
84
|
},
|
|
99
85
|
},
|