@everystack/mcp 0.4.0 → 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/deployment.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
## When to Use
|
|
6
6
|
Read this when deploying an everystack app to AWS for the first time or adding a new stage.
|
|
7
7
|
|
|
8
|
+
If your app.json has `web.output: "server"` (SSR pages or `+api.ts` routes — most Expo Router apps), read everystack://expo-server-deploy next: it covers what deploys where for that shape, including where your `+api.ts` routes run.
|
|
9
|
+
|
|
8
10
|
## Prerequisites
|
|
9
11
|
|
|
10
12
|
- Node.js 20+, pnpm
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Deploying an Expo Router Server App
|
|
2
|
+
|
|
3
|
+
> How `web.output: "server"` maps to AWS: what deploys where, where your `+api.ts` routes run, and where the everystack handler sits among them.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
|
|
7
|
+
Read this when your app.json has `web.output: "server"` (SSR pages, `+api.ts` API routes) and you are deploying to AWS. This is the deploy chapter for the most common Expo Router app shape — if your app is fully static (`web.output: "static"`), everystack://deployment alone is enough.
|
|
8
|
+
|
|
9
|
+
## The Two-Deployable Model
|
|
10
|
+
|
|
11
|
+
The single most important fact: **the Expo server export is NOT the Lambda handler.** There are two deployables with two verbs:
|
|
12
|
+
|
|
13
|
+
| Deployable | Contains | Verb | Frequency |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| Infrastructure + entrypoints | VPC, RDS, buckets, Router, the Lambda entry files in `server/` | `sst deploy` | Rarely (infra changes) |
|
|
16
|
+
| App code | Your entire Expo export: SSR pages, ALL `+api.ts` routes, client bundles | `everystack update --channel <name>` | Every ship (same verb as OTA) |
|
|
17
|
+
|
|
18
|
+
`everystack update` runs the expo export and publishes it as a compressed archive to the Updates bucket. At runtime, the Lambda's SSR fallback downloads that archive to `/tmp` (cached across warm invocations) and serves it with `expo-server` — Expo's own server runtime. Your app code never gets bundled into the Lambda; it ships like an OTA update.
|
|
19
|
+
|
|
20
|
+
## Request Flow
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
CloudFront Router
|
|
24
|
+
└─ Api Lambda (server/api.ts — createPluginLambdaHandler)
|
|
25
|
+
├─ plugin routes claim their paths first (e.g. /api → everystack handler)
|
|
26
|
+
└─ everything else → ssrPlugin fallback → expo-server runs YOUR export:
|
|
27
|
+
├─ SSR pages (with loader())
|
|
28
|
+
└─ your +api.ts routes (mcp+api.ts, app/games/[id]+api.ts, ...)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Your `+api.ts` routes ship and serve as-is.** They ride the published bundle and expo-server dispatches to them inside the Lambda. Nothing has to be collapsed into the everystack handler.
|
|
32
|
+
|
|
33
|
+
## The Lambda Entrypoint
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// server/api.ts — the Function handler for `sst deploy`
|
|
37
|
+
import { createPluginLambdaHandler, ssrPlugin } from '@everystack/server/plugin';
|
|
38
|
+
import { createAppContext, appPlugins } from './context';
|
|
39
|
+
|
|
40
|
+
export const handler = createPluginLambdaHandler({
|
|
41
|
+
context: () => createAppContext(),
|
|
42
|
+
plugins: appPlugins,
|
|
43
|
+
fallback: ssrPlugin(), // serves the published Expo export: SSR + your +api.ts routes
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## One Plugin List, Two Venues
|
|
48
|
+
|
|
49
|
+
Define your plugins once; mount them in both places. Locally, Expo Router serves them through a catch-all route. Deployed, the Lambda mounts the same list and claims those paths BEFORE the fallback — so the deployed `/api` runs with SST Resource linking, never the bundle's copy.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// server/context.ts — shared by local dev and every Lambda
|
|
53
|
+
import { createDb } from '@everystack/server/db';
|
|
54
|
+
import { apiPlugin } from './plugins/api'; // your everystack createHandler, as a plugin
|
|
55
|
+
import { authPlugin } from '@everystack/auth/plugin';
|
|
56
|
+
import { schema } from '../db';
|
|
57
|
+
|
|
58
|
+
export async function createAppContext() {
|
|
59
|
+
const { db } = createDb(schema); // Resource-linked on Lambda, DATABASE_URL locally
|
|
60
|
+
return { db, schema, environment: process.env.ENVIRONMENT ?? 'dev', /* verifyToken, ... */ };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const appPlugins = [authPlugin, apiPlugin];
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// server/plugins/api.ts — the everystack handler as ONE plugin among your routes
|
|
68
|
+
import { createHandler } from '@everystack/api';
|
|
69
|
+
|
|
70
|
+
export async function apiPlugin(ctx) {
|
|
71
|
+
const handler = createHandler(ctx.db, ctx.schema, { basePath: '/api', /* options */ });
|
|
72
|
+
return { routes: [{ path: '/api', handler }] };
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// app/api/[...path]+api.ts — LOCAL DEV venue (Expo Router serves this; the
|
|
78
|
+
// deployed Lambda claims /api first, so this copy never runs in production)
|
|
79
|
+
import { createPluginHandler } from '@everystack/server/plugin';
|
|
80
|
+
import { createAppContext, appPlugins } from '../../server/context';
|
|
81
|
+
|
|
82
|
+
const handler = createPluginHandler({ context: createAppContext, plugins: appPlugins });
|
|
83
|
+
export const GET = handler; export const POST = handler;
|
|
84
|
+
export const PATCH = handler; export const DELETE = handler;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Your OTHER `+api.ts` routes (`app/mcp+api.ts`, `app/games/[id]+api.ts`, ...) need no counterpart: local dev serves them directly, deployed they serve through the fallback.
|
|
88
|
+
|
|
89
|
+
## The Ops Entrypoint (db:migrate, db:seed, console)
|
|
90
|
+
|
|
91
|
+
`everystack db:migrate` invokes a dedicated, privileged Lambda over IAM — it has no public URL and holds the operator credential so the API Lambda never does.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// server/ops.ts
|
|
95
|
+
import { createPluginLambdaHandler, dbPlugin } from '@everystack/server/plugin';
|
|
96
|
+
import { createAppContext, appPlugins } from './context';
|
|
97
|
+
|
|
98
|
+
export const handler = createPluginLambdaHandler({
|
|
99
|
+
http: false, // actions-only: rejects HTTP, answers IAM invokes
|
|
100
|
+
context: () => createAppContext({ admin: true }),
|
|
101
|
+
plugins: [
|
|
102
|
+
...appPlugins,
|
|
103
|
+
dbPlugin({
|
|
104
|
+
migrationsFolder: 'drizzle',
|
|
105
|
+
seed: async (db, schema) => (await import('../db/seed.js')).runSeed(db, schema),
|
|
106
|
+
// ^ lazy import — a top-level import would run at Lambda INIT and crash the boot
|
|
107
|
+
}),
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Minimal sst.config.ts
|
|
113
|
+
|
|
114
|
+
The complete infrastructure for this app shape (V2: server app + PostgreSQL). Copy, rename, deploy.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
/// <reference path="./.sst/platform/config.d.ts" />
|
|
118
|
+
export default $config({
|
|
119
|
+
app(input) {
|
|
120
|
+
return {
|
|
121
|
+
name: 'my-app',
|
|
122
|
+
removal: input?.stage === 'production' ? 'retain' : 'remove',
|
|
123
|
+
home: 'aws',
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
async run() {
|
|
127
|
+
const vpc = new sst.aws.Vpc('Vpc');
|
|
128
|
+
const database = new sst.aws.Postgres('Database', {
|
|
129
|
+
vpc,
|
|
130
|
+
// Local dev: `sst dev` uses this instead of RDS
|
|
131
|
+
dev: { host: 'localhost', port: 5432, username: 'postgres', password: 'postgres', database: 'my_app_dev' },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const updates = new sst.aws.Bucket('Updates'); // published Expo exports (server bundles)
|
|
135
|
+
const clientBundles = new sst.aws.Bucket('ClientBundles', { access: 'cloudfront' });
|
|
136
|
+
|
|
137
|
+
const jwtSecret = new sst.Secret('JwtSecret');
|
|
138
|
+
// Least-privilege API credential — written by `everystack db:provision`.
|
|
139
|
+
const databaseUrl = new sst.Secret('DATABASE_URL');
|
|
140
|
+
|
|
141
|
+
const router = new sst.aws.Router('Router', {
|
|
142
|
+
// Custom domain: Route 53 is the default DNS adapter. With your zone
|
|
143
|
+
// delegated to Route 53, this one line provisions the ACM certificate
|
|
144
|
+
// and DNS records automatically.
|
|
145
|
+
domain: $app.stage === 'production'
|
|
146
|
+
? 'my-app.com'
|
|
147
|
+
: `${$app.stage}.my-app.com`,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const api = new sst.aws.Function('Api', {
|
|
151
|
+
handler: 'server/api.handler',
|
|
152
|
+
runtime: 'nodejs20.x',
|
|
153
|
+
timeout: '30 seconds',
|
|
154
|
+
vpc,
|
|
155
|
+
link: [databaseUrl, updates, clientBundles, jwtSecret],
|
|
156
|
+
url: { router: { instance: router, path: '/' } },
|
|
157
|
+
environment: { ENVIRONMENT: $app.stage, SITE_URL: router.url },
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const ops = new sst.aws.Function('Ops', {
|
|
161
|
+
handler: 'server/ops.handler',
|
|
162
|
+
runtime: 'nodejs20.x',
|
|
163
|
+
timeout: '120 seconds',
|
|
164
|
+
vpc,
|
|
165
|
+
link: [database, databaseUrl, updates, clientBundles, jwtSecret],
|
|
166
|
+
copyFiles: [{ from: 'drizzle', to: 'drizzle' }], // migrations must ship with the ops bundle
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// The CLI discovers resources through these outputs — all five are required.
|
|
170
|
+
return {
|
|
171
|
+
routerUrl: router.url,
|
|
172
|
+
apiFunctionName: api.name,
|
|
173
|
+
opsFunctionName: ops.name,
|
|
174
|
+
updatesBucket: updates.name,
|
|
175
|
+
clientBundlesBucket: clientBundles.name,
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Local vs Deployed Database Connection
|
|
182
|
+
|
|
183
|
+
`createDb()` from `@everystack/server/db` serves both venues with one code path (requires `@everystack/server` >= 0.4.4):
|
|
184
|
+
|
|
185
|
+
- **Deployed:** the linked `DATABASE_URL` secret resolves via SST Resource; TLS defaults to `require` (RDS).
|
|
186
|
+
- **Local:** set `DATABASE_URL=postgres://user@localhost:5432/my_app_dev?sslmode=disable`. An explicit `sslmode` in the URL wins; the handler code does not change.
|
|
187
|
+
|
|
188
|
+
Do not hand-build a `postgres()` client from env vars to escape TLS — the URL is the contract.
|
|
189
|
+
|
|
190
|
+
## Deploy Loop
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
sst deploy --stage dev # infrastructure + entrypoints (rare)
|
|
194
|
+
everystack db:provision --stage dev # mint the least-privilege API credential (once per stage)
|
|
195
|
+
everystack update --channel dev # export + publish the app code (every ship)
|
|
196
|
+
everystack db:migrate --stage dev # run migrations via the ops Lambda
|
|
197
|
+
everystack db:seed --stage dev # dev only
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Subsequent app-code ships are `everystack update` alone — no `sst deploy`, no Lambda redeploy.
|
|
201
|
+
|
|
202
|
+
## Gotchas
|
|
203
|
+
|
|
204
|
+
- **Nothing serves until the first `everystack update`** — the fallback has no bundle to download yet, so `/` returns 404 while `/api` (plugin-claimed) already works.
|
|
205
|
+
- **A path claimed by a plugin never reaches your bundle's route.** If you mount the everystack handler at `/api`, a bundle route at `app/api/foo+api.ts` is shadowed in production (it still serves in local dev unless the catch-all shadows it there too — keep the surfaces aligned).
|
|
206
|
+
- **Lazy-load your seed** (shown above). A top-level `import { runSeed }` in an entrypoint runs at Lambda INIT, tries to reach the database before configuration, and kills the boot.
|
|
207
|
+
- **`copyFiles` the migrations folder** into the ops function, or `db:migrate` fails with ENOENT.
|
|
208
|
+
- **`+api.ts` routes run with the bundle's environment**, not per-route SST links. Resource-linked work (secrets, buckets) belongs in plugins on the Lambda side; keep bundle routes to app logic over the request.
|
package/dist/index.cjs
CHANGED
|
@@ -21652,6 +21652,12 @@ var RESOURCES = [
|
|
|
21652
21652
|
description: "SST deployment: sst.config.ts setup, secrets, stages (dev/production), CloudFront, Lambda configuration, VPC, RDS Aurora Serverless, resource linking.",
|
|
21653
21653
|
filename: "deployment.md"
|
|
21654
21654
|
},
|
|
21655
|
+
{
|
|
21656
|
+
uri: "everystack://expo-server-deploy",
|
|
21657
|
+
name: "Deploying an Expo Router Server App",
|
|
21658
|
+
description: 'How web.output:"server" maps to AWS: the two-deployable model (sst deploy vs everystack update), where +api.ts routes run (the SSR fallback serves the published bundle), the one-plugin-list/two-venues pattern (local catch-all route + deployed Lambda entrypoint), the ops entrypoint for db:migrate, custom domains on the Router, and the local-vs-deployed database URL (sslmode). Read this when deploying any Expo Router app with SSR or +api.ts routes.',
|
|
21659
|
+
filename: "expo-server-deploy.md"
|
|
21660
|
+
},
|
|
21655
21661
|
{
|
|
21656
21662
|
uri: "everystack://admin",
|
|
21657
21663
|
name: "Admin Dashboard",
|
|
@@ -23717,7 +23723,7 @@ async function runGovernanceCli(argv) {
|
|
|
23717
23723
|
}
|
|
23718
23724
|
|
|
23719
23725
|
// src/index.ts
|
|
23720
|
-
var version2 = (true ? "0.4.
|
|
23726
|
+
var version2 = (true ? "0.4.1" : null) ?? "0.3.0-dev";
|
|
23721
23727
|
var INSTRUCTIONS = [
|
|
23722
23728
|
"You govern how any agent builds everystack \u2014 a self-hosted application stack for Expo apps on AWS.",
|
|
23723
23729
|
"Your job is not only to advise but to keep the build on-script: the architecture the maintainer",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
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.4.
|
|
44
|
-
"@everystack/model": "0.4.
|
|
43
|
+
"@everystack/cli": "0.4.19",
|
|
44
|
+
"@everystack/model": "0.4.4"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"test": "jest",
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
## When to Use
|
|
6
6
|
Read this when deploying an everystack app to AWS for the first time or adding a new stage.
|
|
7
7
|
|
|
8
|
+
If your app.json has `web.output: "server"` (SSR pages or `+api.ts` routes — most Expo Router apps), read everystack://expo-server-deploy next: it covers what deploys where for that shape, including where your `+api.ts` routes run.
|
|
9
|
+
|
|
8
10
|
## Prerequisites
|
|
9
11
|
|
|
10
12
|
- Node.js 20+, pnpm
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Deploying an Expo Router Server App
|
|
2
|
+
|
|
3
|
+
> How `web.output: "server"` maps to AWS: what deploys where, where your `+api.ts` routes run, and where the everystack handler sits among them.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
|
|
7
|
+
Read this when your app.json has `web.output: "server"` (SSR pages, `+api.ts` API routes) and you are deploying to AWS. This is the deploy chapter for the most common Expo Router app shape — if your app is fully static (`web.output: "static"`), everystack://deployment alone is enough.
|
|
8
|
+
|
|
9
|
+
## The Two-Deployable Model
|
|
10
|
+
|
|
11
|
+
The single most important fact: **the Expo server export is NOT the Lambda handler.** There are two deployables with two verbs:
|
|
12
|
+
|
|
13
|
+
| Deployable | Contains | Verb | Frequency |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| Infrastructure + entrypoints | VPC, RDS, buckets, Router, the Lambda entry files in `server/` | `sst deploy` | Rarely (infra changes) |
|
|
16
|
+
| App code | Your entire Expo export: SSR pages, ALL `+api.ts` routes, client bundles | `everystack update --channel <name>` | Every ship (same verb as OTA) |
|
|
17
|
+
|
|
18
|
+
`everystack update` runs the expo export and publishes it as a compressed archive to the Updates bucket. At runtime, the Lambda's SSR fallback downloads that archive to `/tmp` (cached across warm invocations) and serves it with `expo-server` — Expo's own server runtime. Your app code never gets bundled into the Lambda; it ships like an OTA update.
|
|
19
|
+
|
|
20
|
+
## Request Flow
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
CloudFront Router
|
|
24
|
+
└─ Api Lambda (server/api.ts — createPluginLambdaHandler)
|
|
25
|
+
├─ plugin routes claim their paths first (e.g. /api → everystack handler)
|
|
26
|
+
└─ everything else → ssrPlugin fallback → expo-server runs YOUR export:
|
|
27
|
+
├─ SSR pages (with loader())
|
|
28
|
+
└─ your +api.ts routes (mcp+api.ts, app/games/[id]+api.ts, ...)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Your `+api.ts` routes ship and serve as-is.** They ride the published bundle and expo-server dispatches to them inside the Lambda. Nothing has to be collapsed into the everystack handler.
|
|
32
|
+
|
|
33
|
+
## The Lambda Entrypoint
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// server/api.ts — the Function handler for `sst deploy`
|
|
37
|
+
import { createPluginLambdaHandler, ssrPlugin } from '@everystack/server/plugin';
|
|
38
|
+
import { createAppContext, appPlugins } from './context';
|
|
39
|
+
|
|
40
|
+
export const handler = createPluginLambdaHandler({
|
|
41
|
+
context: () => createAppContext(),
|
|
42
|
+
plugins: appPlugins,
|
|
43
|
+
fallback: ssrPlugin(), // serves the published Expo export: SSR + your +api.ts routes
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## One Plugin List, Two Venues
|
|
48
|
+
|
|
49
|
+
Define your plugins once; mount them in both places. Locally, Expo Router serves them through a catch-all route. Deployed, the Lambda mounts the same list and claims those paths BEFORE the fallback — so the deployed `/api` runs with SST Resource linking, never the bundle's copy.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// server/context.ts — shared by local dev and every Lambda
|
|
53
|
+
import { createDb } from '@everystack/server/db';
|
|
54
|
+
import { apiPlugin } from './plugins/api'; // your everystack createHandler, as a plugin
|
|
55
|
+
import { authPlugin } from '@everystack/auth/plugin';
|
|
56
|
+
import { schema } from '../db';
|
|
57
|
+
|
|
58
|
+
export async function createAppContext() {
|
|
59
|
+
const { db } = createDb(schema); // Resource-linked on Lambda, DATABASE_URL locally
|
|
60
|
+
return { db, schema, environment: process.env.ENVIRONMENT ?? 'dev', /* verifyToken, ... */ };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const appPlugins = [authPlugin, apiPlugin];
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// server/plugins/api.ts — the everystack handler as ONE plugin among your routes
|
|
68
|
+
import { createHandler } from '@everystack/api';
|
|
69
|
+
|
|
70
|
+
export async function apiPlugin(ctx) {
|
|
71
|
+
const handler = createHandler(ctx.db, ctx.schema, { basePath: '/api', /* options */ });
|
|
72
|
+
return { routes: [{ path: '/api', handler }] };
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// app/api/[...path]+api.ts — LOCAL DEV venue (Expo Router serves this; the
|
|
78
|
+
// deployed Lambda claims /api first, so this copy never runs in production)
|
|
79
|
+
import { createPluginHandler } from '@everystack/server/plugin';
|
|
80
|
+
import { createAppContext, appPlugins } from '../../server/context';
|
|
81
|
+
|
|
82
|
+
const handler = createPluginHandler({ context: createAppContext, plugins: appPlugins });
|
|
83
|
+
export const GET = handler; export const POST = handler;
|
|
84
|
+
export const PATCH = handler; export const DELETE = handler;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Your OTHER `+api.ts` routes (`app/mcp+api.ts`, `app/games/[id]+api.ts`, ...) need no counterpart: local dev serves them directly, deployed they serve through the fallback.
|
|
88
|
+
|
|
89
|
+
## The Ops Entrypoint (db:migrate, db:seed, console)
|
|
90
|
+
|
|
91
|
+
`everystack db:migrate` invokes a dedicated, privileged Lambda over IAM — it has no public URL and holds the operator credential so the API Lambda never does.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// server/ops.ts
|
|
95
|
+
import { createPluginLambdaHandler, dbPlugin } from '@everystack/server/plugin';
|
|
96
|
+
import { createAppContext, appPlugins } from './context';
|
|
97
|
+
|
|
98
|
+
export const handler = createPluginLambdaHandler({
|
|
99
|
+
http: false, // actions-only: rejects HTTP, answers IAM invokes
|
|
100
|
+
context: () => createAppContext({ admin: true }),
|
|
101
|
+
plugins: [
|
|
102
|
+
...appPlugins,
|
|
103
|
+
dbPlugin({
|
|
104
|
+
migrationsFolder: 'drizzle',
|
|
105
|
+
seed: async (db, schema) => (await import('../db/seed.js')).runSeed(db, schema),
|
|
106
|
+
// ^ lazy import — a top-level import would run at Lambda INIT and crash the boot
|
|
107
|
+
}),
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Minimal sst.config.ts
|
|
113
|
+
|
|
114
|
+
The complete infrastructure for this app shape (V2: server app + PostgreSQL). Copy, rename, deploy.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
/// <reference path="./.sst/platform/config.d.ts" />
|
|
118
|
+
export default $config({
|
|
119
|
+
app(input) {
|
|
120
|
+
return {
|
|
121
|
+
name: 'my-app',
|
|
122
|
+
removal: input?.stage === 'production' ? 'retain' : 'remove',
|
|
123
|
+
home: 'aws',
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
async run() {
|
|
127
|
+
const vpc = new sst.aws.Vpc('Vpc');
|
|
128
|
+
const database = new sst.aws.Postgres('Database', {
|
|
129
|
+
vpc,
|
|
130
|
+
// Local dev: `sst dev` uses this instead of RDS
|
|
131
|
+
dev: { host: 'localhost', port: 5432, username: 'postgres', password: 'postgres', database: 'my_app_dev' },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const updates = new sst.aws.Bucket('Updates'); // published Expo exports (server bundles)
|
|
135
|
+
const clientBundles = new sst.aws.Bucket('ClientBundles', { access: 'cloudfront' });
|
|
136
|
+
|
|
137
|
+
const jwtSecret = new sst.Secret('JwtSecret');
|
|
138
|
+
// Least-privilege API credential — written by `everystack db:provision`.
|
|
139
|
+
const databaseUrl = new sst.Secret('DATABASE_URL');
|
|
140
|
+
|
|
141
|
+
const router = new sst.aws.Router('Router', {
|
|
142
|
+
// Custom domain: Route 53 is the default DNS adapter. With your zone
|
|
143
|
+
// delegated to Route 53, this one line provisions the ACM certificate
|
|
144
|
+
// and DNS records automatically.
|
|
145
|
+
domain: $app.stage === 'production'
|
|
146
|
+
? 'my-app.com'
|
|
147
|
+
: `${$app.stage}.my-app.com`,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const api = new sst.aws.Function('Api', {
|
|
151
|
+
handler: 'server/api.handler',
|
|
152
|
+
runtime: 'nodejs20.x',
|
|
153
|
+
timeout: '30 seconds',
|
|
154
|
+
vpc,
|
|
155
|
+
link: [databaseUrl, updates, clientBundles, jwtSecret],
|
|
156
|
+
url: { router: { instance: router, path: '/' } },
|
|
157
|
+
environment: { ENVIRONMENT: $app.stage, SITE_URL: router.url },
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const ops = new sst.aws.Function('Ops', {
|
|
161
|
+
handler: 'server/ops.handler',
|
|
162
|
+
runtime: 'nodejs20.x',
|
|
163
|
+
timeout: '120 seconds',
|
|
164
|
+
vpc,
|
|
165
|
+
link: [database, databaseUrl, updates, clientBundles, jwtSecret],
|
|
166
|
+
copyFiles: [{ from: 'drizzle', to: 'drizzle' }], // migrations must ship with the ops bundle
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// The CLI discovers resources through these outputs — all five are required.
|
|
170
|
+
return {
|
|
171
|
+
routerUrl: router.url,
|
|
172
|
+
apiFunctionName: api.name,
|
|
173
|
+
opsFunctionName: ops.name,
|
|
174
|
+
updatesBucket: updates.name,
|
|
175
|
+
clientBundlesBucket: clientBundles.name,
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Local vs Deployed Database Connection
|
|
182
|
+
|
|
183
|
+
`createDb()` from `@everystack/server/db` serves both venues with one code path (requires `@everystack/server` >= 0.4.4):
|
|
184
|
+
|
|
185
|
+
- **Deployed:** the linked `DATABASE_URL` secret resolves via SST Resource; TLS defaults to `require` (RDS).
|
|
186
|
+
- **Local:** set `DATABASE_URL=postgres://user@localhost:5432/my_app_dev?sslmode=disable`. An explicit `sslmode` in the URL wins; the handler code does not change.
|
|
187
|
+
|
|
188
|
+
Do not hand-build a `postgres()` client from env vars to escape TLS — the URL is the contract.
|
|
189
|
+
|
|
190
|
+
## Deploy Loop
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
sst deploy --stage dev # infrastructure + entrypoints (rare)
|
|
194
|
+
everystack db:provision --stage dev # mint the least-privilege API credential (once per stage)
|
|
195
|
+
everystack update --channel dev # export + publish the app code (every ship)
|
|
196
|
+
everystack db:migrate --stage dev # run migrations via the ops Lambda
|
|
197
|
+
everystack db:seed --stage dev # dev only
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Subsequent app-code ships are `everystack update` alone — no `sst deploy`, no Lambda redeploy.
|
|
201
|
+
|
|
202
|
+
## Gotchas
|
|
203
|
+
|
|
204
|
+
- **Nothing serves until the first `everystack update`** — the fallback has no bundle to download yet, so `/` returns 404 while `/api` (plugin-claimed) already works.
|
|
205
|
+
- **A path claimed by a plugin never reaches your bundle's route.** If you mount the everystack handler at `/api`, a bundle route at `app/api/foo+api.ts` is shadowed in production (it still serves in local dev unless the catch-all shadows it there too — keep the surfaces aligned).
|
|
206
|
+
- **Lazy-load your seed** (shown above). A top-level `import { runSeed }` in an entrypoint runs at Lambda INIT, tries to reach the database before configuration, and kills the boot.
|
|
207
|
+
- **`copyFiles` the migrations folder** into the ops function, or `db:migrate` fails with ENOENT.
|
|
208
|
+
- **`+api.ts` routes run with the bundle's environment**, not per-route SST links. Resource-linked work (secrets, buckets) belongs in plugins on the Lambda side; keep bundle routes to app logic over the request.
|
package/src/resources/index.ts
CHANGED
|
@@ -140,6 +140,13 @@ const RESOURCES: ResourceDef[] = [
|
|
|
140
140
|
'SST deployment: sst.config.ts setup, secrets, stages (dev/production), CloudFront, Lambda configuration, VPC, RDS Aurora Serverless, resource linking.',
|
|
141
141
|
filename: 'deployment.md',
|
|
142
142
|
},
|
|
143
|
+
{
|
|
144
|
+
uri: 'everystack://expo-server-deploy',
|
|
145
|
+
name: 'Deploying an Expo Router Server App',
|
|
146
|
+
description:
|
|
147
|
+
'How web.output:"server" maps to AWS: the two-deployable model (sst deploy vs everystack update), where +api.ts routes run (the SSR fallback serves the published bundle), the one-plugin-list/two-venues pattern (local catch-all route + deployed Lambda entrypoint), the ops entrypoint for db:migrate, custom domains on the Router, and the local-vs-deployed database URL (sslmode). Read this when deploying any Expo Router app with SSR or +api.ts routes.',
|
|
148
|
+
filename: 'expo-server-deploy.md',
|
|
149
|
+
},
|
|
143
150
|
{
|
|
144
151
|
uri: 'everystack://admin',
|
|
145
152
|
name: 'Admin Dashboard',
|