@everystack/mcp 0.3.3 → 0.4.1
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/deployment.md +2 -0
- package/dist/derived-objects.md +225 -0
- package/dist/expo-server-deploy.md +208 -0
- package/dist/index.cjs +118 -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/deployment.md +2 -0
- package/src/resources/derived-objects.md +225 -0
- package/src/resources/expo-server-deploy.md +208 -0
- package/src/resources/index.ts +22 -1
- package/src/resources/project-claude-md.md +15 -12
- package/src/resources/schema-patterns.md +92 -106
|
@@ -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).
|