spfn 0.2.0-beta.9 → 0.3.0-beta.2
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/LICENSE +1 -1
- package/README.md +626 -204
- package/bin/spfn.js +53 -4
- package/dist/index.js +4183 -2281
- package/dist/templates/.dockerignore +1 -0
- package/dist/templates/Dockerfile +1 -1
- package/dist/templates/README.md +94 -0
- package/dist/templates/lib/api-client.ts +1 -1
- package/dist/templates/modes/full/src/app/auth/callback/page.tsx +1 -0
- package/dist/templates/modes/full/src/app/login/page.tsx +68 -0
- package/dist/templates/modes/full/src/i18n/catalogs.ts +16 -0
- package/dist/templates/modes/full/src/i18n/server.ts +9 -0
- package/dist/templates/modes/full/src/server/config/env.config.ts +23 -0
- package/dist/templates/modes/full/src/server/mcp.ts +75 -0
- package/dist/templates/modes/full/src/server/router.ts +32 -0
- package/dist/templates/modes/full/src/server/routes/examples.ts +94 -0
- package/dist/templates/modes/full/src/server/routes/health.ts +9 -0
- package/dist/templates/modes/full/src/server/routes/root.ts +15 -0
- package/dist/templates/modes/full/src/server/server.config.ts +14 -0
- package/dist/templates/server/config/env.config.ts +10 -75
- package/dist/templates/server/entities/config.ts +1 -1
- package/dist/templates/server/entities/example.entity.ts +1 -1
- package/dist/templates/server/repositories/example.repository.ts +1 -1
- package/dist/templates/server/router.ts +2 -3
- package/dist/templates/server/routes/examples.ts +2 -12
- package/dist/templates/server/routes/health.ts +1 -1
- package/dist/templates/server/routes/root.ts +1 -2
- package/dist/templates/server/server.config.ts +2 -2
- package/dist/templates/vercel/npmrc +4 -0
- package/dist/templates/vercel/route.ts +53 -0
- package/dist/templates/vercel/vercel.json +6 -0
- package/package.json +35 -23
- package/dist/commands/generate/templates/contract.template +0 -58
- package/dist/commands/generate/templates/entity.template +0 -28
- package/dist/commands/generate/templates/init-migration.template +0 -3
- package/dist/commands/generate/templates/repository.template +0 -56
- package/dist/commands/generate/templates/route.template +0 -65
- package/dist/commands/generate/templates/schema.template +0 -13
- package/dist/templates/Dockerfile.optimized +0 -67
- package/dist/templates/config/index.ts +0 -22
- package/dist/templates/config/schema.ts +0 -42
package/README.md
CHANGED
|
@@ -1,260 +1,682 @@
|
|
|
1
|
-
# spfn
|
|
1
|
+
# spfn — the SPFN CLI (backend layer for Next.js)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`spfn` takes a Next.js idea from prototype to production with a consistent full-stack
|
|
4
|
+
architecture. It can scaffold either a core-only backend or a production baseline with
|
|
5
|
+
authentication, internationalization, and an agent-facing MCP endpoint, then runs the
|
|
6
|
+
dev/build/start lifecycle, database tooling, RPC codegen, and environment validation.
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
Consistent is the point rather than a nicety: what it scaffolds is one fixed shape per
|
|
9
|
+
feature, so a coding agent has no architecture left to invent and the codebase does not
|
|
10
|
+
acquire several. That problem has a name —
|
|
11
|
+
[architecture drift](https://superfunction.xyz/architecture-drift).
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
> Beta: install with the `@beta` tag (`spfn@beta`). The binary is `spfn`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
8
16
|
|
|
9
|
-
|
|
17
|
+
No global install needed — run through your package manager's dlx/npx:
|
|
10
18
|
|
|
11
|
-
### Quick Start (New Project)
|
|
12
19
|
```bash
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
cd my-app
|
|
16
|
-
docker compose up -d
|
|
17
|
-
npm run spfn:dev
|
|
20
|
+
npx spfn@beta <command>
|
|
21
|
+
pnpm dlx spfn@beta <command>
|
|
18
22
|
```
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
Or add it as a project dependency (`spfn init`/`spfn create` do this for you), then
|
|
25
|
+
call it via `pnpm spfn <command>` / `npm run spfn:<script>`.
|
|
26
|
+
|
|
27
|
+
Requirements: Node.js 20+ in both modes, Next.js 16.2.11+ (App Router, `src/` dir),
|
|
28
|
+
PostgreSQL 14+ (Redis optional). Next.js 15
|
|
29
|
+
is not supported — see [the root README](../../README.md#what-do-i-need-installed).
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
21
33
|
```bash
|
|
22
|
-
#
|
|
23
|
-
npx spfn@
|
|
34
|
+
# Prototype-to-Production baseline: core + auth + i18n + MCP
|
|
35
|
+
npx spfn@beta create my-app --mode full
|
|
36
|
+
cd my-app
|
|
37
|
+
docker compose up -d # Postgres + Redis
|
|
38
|
+
# .env.local & .env.server are generated — keep both gitignored
|
|
39
|
+
pnpm spfn db migrate # Apply auth migrations
|
|
40
|
+
pnpm spfn:dev # Next.js :3790 + SPFN API :8790
|
|
24
41
|
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
# Core-only full-stack skeleton
|
|
43
|
+
npx spfn@beta create my-api --mode bare
|
|
44
|
+
|
|
45
|
+
# Add SPFN to an existing Next.js project
|
|
46
|
+
npx spfn@beta init --mode full
|
|
28
47
|
```
|
|
29
48
|
|
|
49
|
+
The package manager is auto-detected (pnpm > yarn > bun > npm) from lockfiles; override
|
|
50
|
+
with `--pm`. In a pnpm workspace, `create` installs from the workspace root.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
30
54
|
## Commands
|
|
31
55
|
|
|
32
|
-
|
|
56
|
+
Registered top-level commands: `create`, `init`, `add`, `dev`, `build`, `start`,
|
|
57
|
+
`provision`, `codegen`, `contract`, `key`, `setup`, `db`, `env`, `ops`, `secret`.
|
|
58
|
+
|
|
59
|
+
### `spfn create <name>`
|
|
60
|
+
|
|
61
|
+
Runs `create-next-app` with SPFN-recommended flags (TypeScript, App Router, `src/`,
|
|
62
|
+
Tailwind, import alias `@/*`, no ESLint), sets up SVGR icons, then runs `init`.
|
|
63
|
+
|
|
64
|
+
Choose `full` for the recommended Prototype-to-Production baseline or `bare` for the
|
|
65
|
+
historical core-only skeleton. Without `--mode`, interactive runs show a mode selector
|
|
66
|
+
with `full` recommended. For backward compatibility, non-interactive `--yes` runs without
|
|
67
|
+
an explicit mode continue to generate `bare`; automation that wants full should always
|
|
68
|
+
pass `--mode full`.
|
|
69
|
+
|
|
70
|
+
| Option | Description |
|
|
71
|
+
|--------|-------------|
|
|
72
|
+
| `--pm <manager>` | Force package manager: `npm` \| `pnpm` \| `yarn` \| `bun` |
|
|
73
|
+
| `--shadcn` | Also run `shadcn init` |
|
|
74
|
+
| `--mode <mode>` | `bare` (core only) \| `full` (core, auth, i18n, MCP) |
|
|
75
|
+
| `--skip-install` | Skip dependency install |
|
|
76
|
+
| `--skip-git` | Skip `git init` |
|
|
77
|
+
| `-y, --yes` | Skip prompts, use defaults |
|
|
78
|
+
|
|
79
|
+
### `spfn init`
|
|
80
|
+
|
|
81
|
+
Adds SPFN to an existing Next.js project: copies the selected server templates, wires the RPC
|
|
82
|
+
proxy route, Docker files, deploy + codegen config, updates `package.json` scripts/deps,
|
|
83
|
+
and installs. Full mode also adds the `/_auth/:path*` → SPFN API rewrite to
|
|
84
|
+
`next.config` (OAuth callbacks return to the app origin; merged manually if a `rewrites()`
|
|
85
|
+
already exists). See [Scaffold structure](#scaffold-structure) for what lands on disk.
|
|
86
|
+
|
|
87
|
+
| Option | Description |
|
|
88
|
+
|--------|-------------|
|
|
89
|
+
| `--mode <mode>` | `bare` (core only) \| `full` (core, auth, i18n, MCP) |
|
|
90
|
+
| `-y, --yes` | Skip prompts, use defaults |
|
|
91
|
+
|
|
92
|
+
Generated projects pin `drizzle-orm` and `drizzle-kit` to `1.0.0-rc.4`, matching
|
|
93
|
+
`@spfn/core` and the rest of the published SPFN database packages.
|
|
94
|
+
|
|
95
|
+
### `spfn add <package>`
|
|
96
|
+
|
|
97
|
+
Installs an SPFN ecosystem package and applies its pre-built migrations. The package
|
|
98
|
+
name must be scoped (contain `/`).
|
|
99
|
+
|
|
33
100
|
```bash
|
|
34
|
-
spfn
|
|
35
|
-
spfn
|
|
36
|
-
spfn create my-app --shadcn # Include shadcn/ui component library
|
|
101
|
+
pnpm spfn add @spfn/cms
|
|
102
|
+
pnpm spfn add @mycompany/spfn-analytics
|
|
37
103
|
```
|
|
38
104
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
spfn
|
|
105
|
+
How it works: if not already present it installs the package, then reads the package's
|
|
106
|
+
`spfn` field in its `package.json` (`migrations`, `setupMessage`) and applies any
|
|
107
|
+
function migrations to `DATABASE_URL`. If `DATABASE_URL` is unset, migration is skipped
|
|
108
|
+
with a hint to run `spfn db push` later. Works with published and workspace packages.
|
|
109
|
+
|
|
110
|
+
### `spfn add vercel`
|
|
111
|
+
|
|
112
|
+
`vercel` is not a package — it is a built-in target, and the one argument to `add` that is
|
|
113
|
+
not a scoped package name. It scaffolds the three files a Next.js + SPFN app needs to run
|
|
114
|
+
its backend as Vercel Functions:
|
|
115
|
+
|
|
116
|
+
| File | What it is |
|
|
117
|
+
|------|------------|
|
|
118
|
+
| `src/app/api/backend/[[...route]]/route.ts` | `hono/vercel` adapter, mounts the SPFN app under `/api/backend` |
|
|
119
|
+
| `vercel.json` | build config (`pnpm spfn:build`) |
|
|
120
|
+
| `.npmrc` | `@spfn` registry auth, reading `GITEA_NPM_TOKEN` from the environment — never committed |
|
|
121
|
+
|
|
122
|
+
Existing files are never overwritten; they are reported and skipped. The runtime behind
|
|
123
|
+
the adapter is `createServerlessApp()` from `@spfn/core/server`. Afterwards, point
|
|
124
|
+
`SPFN_API_URL` at `https://<your-domain>/api/backend`, and make sure `hono` is a direct
|
|
125
|
+
dependency of the app so `hono/vercel` resolves.
|
|
126
|
+
|
|
127
|
+
### `spfn dev`
|
|
128
|
+
|
|
129
|
+
Starts the SPFN server + Next.js (and a codegen watcher). The server must report ready
|
|
130
|
+
(via a `.spfn/server-ready` signal file) before Next.js launches. Runs through `tsx`,
|
|
131
|
+
no pre-build needed.
|
|
132
|
+
|
|
133
|
+
| Option | Description | Default |
|
|
134
|
+
|--------|-------------|---------|
|
|
135
|
+
| `--server-only` | Run only the SPFN/Hono server (also auto-selected if Next.js isn't a dependency) | off |
|
|
136
|
+
| `--watch` | Restart the server on `src/server` changes (chokidar) | off |
|
|
137
|
+
| `-p, --port <port>` | Server port | from `server.config.ts` / env (`4000` in server-only fallback) |
|
|
138
|
+
| `-H, --host <host>` | Server host | `localhost` |
|
|
139
|
+
| `--routes <path>` | Routes directory path | server default |
|
|
140
|
+
| `--allow-pending-migrations` | Start even when migrations are pending (they are listed as a warning) | off |
|
|
141
|
+
|
|
142
|
+
Note: hot reload is **off by default** — pass `--watch` to restart on file changes.
|
|
143
|
+
|
|
144
|
+
Pending migrations stop the boot — see [Database](#spfn-db) for what the refusal looks
|
|
145
|
+
like and how to override it.
|
|
146
|
+
|
|
147
|
+
### `spfn build`
|
|
148
|
+
|
|
149
|
+
Runs codegen, builds Next.js (via the project's `build` script), and compiles
|
|
150
|
+
`src/server/**/*.ts` → `.spfn/server` with tsup. Also writes `.spfn/prod-server.mjs`
|
|
151
|
+
(the production entry consumed by `spfn start`).
|
|
152
|
+
|
|
153
|
+
| Option | Description |
|
|
154
|
+
|--------|-------------|
|
|
155
|
+
| `--server-only` | Build only the SPFN server (skip Next.js) |
|
|
156
|
+
| `--next-only` | Build only Next.js (skip the SPFN server) |
|
|
157
|
+
| `--turbo` | Use Turbopack for the Next.js build |
|
|
158
|
+
|
|
159
|
+
### `spfn start`
|
|
160
|
+
|
|
161
|
+
Starts the production servers from build output. Requires `spfn build` first — it errors
|
|
162
|
+
if `.spfn/server`, `.spfn/prod-server.mjs`, or `.next` are missing.
|
|
163
|
+
|
|
164
|
+
| Option | Description | Default |
|
|
165
|
+
|--------|-------------|---------|
|
|
166
|
+
| `--server-only` | Run only the SPFN server | off |
|
|
167
|
+
| `--next-only` | Run only Next.js | off |
|
|
168
|
+
| `-p, --port <port>` | SPFN server port (sets `SPFN_PORT`) | `8790` |
|
|
169
|
+
| `-h, --host <host>` | SPFN server host (sets `SPFN_HOST`) | `0.0.0.0` |
|
|
170
|
+
| `--allow-pending-migrations` | Start even when migrations are pending (they are listed as a warning) | off |
|
|
171
|
+
|
|
172
|
+
Next.js is started on `0.0.0.0:3790`. Both run together via `concurrently --kill-others`.
|
|
173
|
+
|
|
174
|
+
Pending migrations stop the boot unless `--allow-pending-migrations` or
|
|
175
|
+
`SPFN_ALLOW_PENDING_MIGRATIONS=true` is set — see [Database](#spfn-db). `--next-only`
|
|
176
|
+
skips the check: no SPFN server starts, so nothing can drift.
|
|
177
|
+
|
|
178
|
+
### `spfn codegen`
|
|
179
|
+
|
|
180
|
+
Manages code generators driven by `.spfnrc.ts`. The default generator is
|
|
181
|
+
`@spfn/core:route-map`, which emits `src/generated/route-map.ts` from `src/server/router.ts`
|
|
182
|
+
so the RPC proxy can resolve routes without importing server code. Generators also run
|
|
183
|
+
automatically during `spfn dev` and `spfn build`.
|
|
184
|
+
|
|
185
|
+
| Subcommand | Description |
|
|
186
|
+
|------------|-------------|
|
|
187
|
+
| `codegen init` | Create `.spfnrc.ts` (`--with-example` shows custom-generator usage) |
|
|
188
|
+
| `codegen list` (`ls`) | List configured generators and their watch patterns |
|
|
189
|
+
| `codegen run` | Run all generators once (no watch) |
|
|
190
|
+
|
|
191
|
+
`package.json` exposes this as the `codegen` script (`spfn codegen run`).
|
|
192
|
+
|
|
193
|
+
To add a custom generator, implement the `Generator` interface from `@spfn/core/codegen`
|
|
194
|
+
and reference it in `.spfnrc.ts`:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
// .spfnrc.ts
|
|
198
|
+
import { defineConfig, defineGenerator } from '@spfn/core/codegen';
|
|
199
|
+
|
|
200
|
+
export default defineConfig({
|
|
201
|
+
generators: [
|
|
202
|
+
defineGenerator({
|
|
203
|
+
name: '@spfn/core:route-map',
|
|
204
|
+
routerPath: './src/server/router.ts',
|
|
205
|
+
outputPath: './src/generated/route-map.ts',
|
|
206
|
+
}),
|
|
207
|
+
],
|
|
208
|
+
});
|
|
43
209
|
```
|
|
44
210
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
**SPFN's Contract-Based Architecture:**
|
|
56
|
-
- **Contracts** (`src/lib/contracts/`): Define API endpoints with absolute paths (e.g., `/users/:id`)
|
|
57
|
-
- **Handlers** (`src/server/routes/`): Import contracts and implement business logic
|
|
58
|
-
- **Frontend**: Import contracts for type-safe API calls
|
|
59
|
-
- **Auto-generated Client**: `src/lib/api/` is auto-generated from contracts (via `spfn dev` or `spfn build`)
|
|
60
|
-
|
|
61
|
-
### Generate Function Modules
|
|
211
|
+
### `spfn contract`
|
|
212
|
+
|
|
213
|
+
Manages the route contract — what a **separately deployed** client (a mobile app, an external
|
|
214
|
+
API consumer) is promised. A web client needs none of this: it derives its types from
|
|
215
|
+
`AppRouter` in the same build, so a broken response already fails the compile.
|
|
216
|
+
|
|
217
|
+
Requires the `@spfn/core:contract` generator in `.spfnrc.ts`. Every command regenerates the
|
|
218
|
+
contract from the router first, so a stale `contracts/current.json` is never what gets checked
|
|
219
|
+
or released.
|
|
220
|
+
|
|
62
221
|
```bash
|
|
63
|
-
|
|
64
|
-
spfn
|
|
222
|
+
# Regenerate and compare against the newest released snapshot
|
|
223
|
+
spfn contract check
|
|
65
224
|
|
|
66
|
-
#
|
|
67
|
-
spfn
|
|
68
|
-
|
|
225
|
+
# Cut a release — writes contracts/released/1.3.0.json. Commit it.
|
|
226
|
+
spfn contract release 1.3.0
|
|
227
|
+
|
|
228
|
+
# List released snapshots
|
|
229
|
+
spfn contract list
|
|
69
230
|
```
|
|
70
231
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
232
|
+
| Subcommand | Description | Exit code |
|
|
233
|
+
|------------|-------------|-----------|
|
|
234
|
+
| `contract check` | Compares the current contract against the newest released snapshot | 1 when a promise is broken |
|
|
235
|
+
| `contract release <version>` | Writes the snapshot every later build is compared against | 1 when the contract is broken, the version already exists, or it is not newer than the newest one |
|
|
236
|
+
| `contract list` (`ls`) | Lists released snapshots | 0 |
|
|
237
|
+
|
|
238
|
+
`--dir <path>` overrides the contracts directory; by default it comes from the generator's
|
|
239
|
+
`outputDir` in `.spfnrc.ts`.
|
|
240
|
+
|
|
241
|
+
**`spfn build` runs the same gate.** A broken contract fails the build with a non-zero exit
|
|
242
|
+
code — that is the point of hanging the check off codegen rather than leaving it to a
|
|
243
|
+
separate step.
|
|
244
|
+
|
|
245
|
+
See [`@spfn/core` contract docs](../core/src/contract/README.md) for the case table and the
|
|
246
|
+
removal rules.
|
|
247
|
+
|
|
248
|
+
### `spfn db`
|
|
249
|
+
|
|
250
|
+
Wraps Drizzle Kit with auto-generated config. Most commands read `DATABASE_URL` from the
|
|
251
|
+
loaded `.env` chain.
|
|
252
|
+
|
|
253
|
+
| Subcommand | Description |
|
|
254
|
+
|------------|-------------|
|
|
255
|
+
| `db generate` (`g`) | Generate migrations from schema changes (timestamp-prefixed) |
|
|
256
|
+
| `db push` | Diff with Drizzle Kit's current PostgreSQL engine and apply the selected DDL atomically. Destructive changes need confirmation; `--force` applies them, `--dry-run` previews |
|
|
257
|
+
| `db migrate` (`m`) | Run pending migrations. `--with-backup` snapshots first |
|
|
258
|
+
| `db status` | Show which migrations are applied and which are pending, for the project and for each installed function package |
|
|
259
|
+
| `db studio` | Open Drizzle Studio. `-p, --port` (auto-finds a free port) |
|
|
260
|
+
| `db check` | Verify the database connection |
|
|
261
|
+
| `db drop` | Drop all tables — **destructive**, double-prompts (see [Pitfalls](#pitfalls)) |
|
|
262
|
+
| `db backup` | Create a backup (`-f sql|custom`, `-o`, `-s`, `--data-only`, `--schema-only`, `--tag`, `--env`) |
|
|
263
|
+
| `db restore [file]` | Restore from a backup (`--drop`, `-s`, `--data-only`, `--schema-only`, `-v`) |
|
|
264
|
+
| `db backup:list` | List backups |
|
|
265
|
+
| `db backup:clean` | Prune backups (`-k, --keep <n>`, `-o, --older-than <days>`) |
|
|
266
|
+
| `db reindex` | Convert sequential migration prefixes to timestamps (`--dry-run`) |
|
|
267
|
+
|
|
268
|
+
> `db push` is for development. For production, use `db generate` + `db migrate` to keep
|
|
269
|
+
> migration history.
|
|
270
|
+
|
|
271
|
+
**A server refuses to start while migrations are pending.** Bumping `@spfn/auth` and
|
|
272
|
+
skipping `db migrate` used to boot fine, pass the health check, and then fail every
|
|
273
|
+
request that touched a new column as an opaque 500. `spfn dev` and `spfn start` now
|
|
274
|
+
compare the migrations each installed function package ships (and `src/server/drizzle`,
|
|
275
|
+
where present) against what the database records as applied, print the ones still
|
|
276
|
+
waiting, and stop:
|
|
100
277
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
278
|
+
```
|
|
279
|
+
❌ Refusing to start: 1 pending migration(s) in @spfn/auth
|
|
280
|
+
@spfn/auth: 1 pending migration(s) (12/13 applied)
|
|
281
|
+
- 20260805143152_client_identity
|
|
104
282
|
|
|
105
|
-
|
|
106
|
-
spfn add @spfn/blog
|
|
283
|
+
Run: pnpm spfn db migrate
|
|
107
284
|
```
|
|
108
285
|
|
|
109
|
-
|
|
286
|
+
`--allow-pending-migrations` starts anyway and logs the same list as a warning.
|
|
287
|
+
`SPFN_ALLOW_PENDING_MIGRATIONS=true` does the same where no flag can be passed — a
|
|
288
|
+
container's env, a CI job. The check is skipped when the app initializes no database
|
|
289
|
+
or no package ships migrations, and a database it cannot reach is reported as
|
|
290
|
+
"could not verify", never as drift.
|
|
291
|
+
|
|
292
|
+
`db push` and `db migrate` also replay migrations shipped by installed SPFN function
|
|
293
|
+
packages (`@spfn/auth`, `@spfn/cms`, …) into per-package tracking tables
|
|
294
|
+
(`drizzle.__spfn_fn_<pkg>_migrations`). The CLI applies these with a built-in runner
|
|
295
|
+
that reads both migration layouts — drizzle-kit ≤0.31 (`NNNN_name.sql` +
|
|
296
|
+
`meta/_journal.json`) and drizzle-kit 1.0 (`<timestamp>_name/migration.sql`) — so a
|
|
297
|
+
package's layout never has to match the CLI's bundled drizzle version. `db push`
|
|
298
|
+
validates every package's migration folder before applying the project schema, and a
|
|
299
|
+
function-migration failure after a successful schema apply exits 1 with a message
|
|
300
|
+
making clear the project schema was already committed.
|
|
301
|
+
|
|
302
|
+
Database TLS is controlled by `DATABASE_URL`. Loopback URLs (`localhost`, `127.0.0.1`,
|
|
303
|
+
and `::1`) default to `ssl: false`; add an explicit `sslmode` when the local server uses
|
|
304
|
+
TLS. For a TLS connection with a self-signed certificate, set
|
|
305
|
+
`SPFN_DB_INSECURE_TLS=1` to disable certificate verification. This opt-in never enables
|
|
306
|
+
TLS by itself and `sslmode=disable` remains authoritative.
|
|
307
|
+
|
|
308
|
+
### `spfn env`
|
|
309
|
+
|
|
310
|
+
Schema-driven environment variable tooling (schema comes from a package's `envSchema`,
|
|
311
|
+
default `@spfn/core`). Routes vars to the right file: `NEXT_PUBLIC_*` → `.env`/`.env.local`,
|
|
312
|
+
server vars → `.env.server`.
|
|
313
|
+
|
|
314
|
+
| Subcommand | Description |
|
|
315
|
+
|------------|-------------|
|
|
316
|
+
| `env list` | List vars from the schema (`-g` groups by target file) |
|
|
317
|
+
| `env stats` | Show variable statistics |
|
|
318
|
+
| `env search <query>` | Search vars by key or description |
|
|
319
|
+
| `env init` | Generate `.env` template files (`-e <env>` for per-env, `-f` to overwrite) |
|
|
320
|
+
| `env check` | Check `.env` files against the schema (`-e <env>` for a full env chain) |
|
|
321
|
+
| `env validate` | Validate `process.env` against the schema — for CI/CD (`-e <env>`, `-s` strict) |
|
|
322
|
+
|
|
323
|
+
All accept `-p, --package <pkg>` (`env validate` uses `-p, --packages <pkgs...>`).
|
|
324
|
+
|
|
325
|
+
### `spfn key [preset]`
|
|
326
|
+
|
|
327
|
+
Generate cryptographically random secrets (base64url, 256-bit default).
|
|
328
|
+
|
|
110
329
|
```bash
|
|
111
|
-
spfn
|
|
112
|
-
spfn
|
|
113
|
-
spfn
|
|
330
|
+
spfn key # generic 256-bit secret
|
|
331
|
+
spfn key auth-encryption -c # preset key, copy to clipboard
|
|
332
|
+
spfn key --list # list presets
|
|
333
|
+
spfn key gen -b 64 # raw value only, no metadata (alias of `key generate`)
|
|
114
334
|
```
|
|
115
335
|
|
|
116
|
-
|
|
117
|
-
1
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
336
|
+
Presets: `auth-encryption`, `nextauth-secret`, `jwt-secret`, `session-secret`, `api-key`.
|
|
337
|
+
Options: `-l, --list`, `-b, --bytes <n>` (1–128), `-e, --env <name>`, `-c, --copy`.
|
|
338
|
+
The command prints the value to stdout for you to paste into an env file — it does **not**
|
|
339
|
+
write any file.
|
|
340
|
+
|
|
341
|
+
### `spfn secret`
|
|
342
|
+
|
|
343
|
+
Unified secret management: local secrets live in the OS keychain, deployed secrets in
|
|
344
|
+
encrypted SOPS files. The runtime never sees a reference — `spfn dev` injects local
|
|
345
|
+
values into the server process and GitOps injects them in production, so the app always
|
|
346
|
+
reads plain `process.env`.
|
|
347
|
+
|
|
348
|
+
| Subcommand | Description |
|
|
349
|
+
|------------|-------------|
|
|
350
|
+
| `secret set [key]` | Store a value (masked prompt). `--env local` → keychain; other envs → SOPS |
|
|
351
|
+
| `secret list` | List declared secrets and their status per env (never prints values) |
|
|
352
|
+
| `secret generate [key]` | Mint values for schema secrets with a `generate` strategy (`-a/--all`) |
|
|
353
|
+
| `secret rotate [key]` | Rotate values; external secrets are flagged for manual reissue (`-a/--all`) |
|
|
354
|
+
| `secret keygen` | Generate an age key pair for the SOPS no-cloud backend |
|
|
355
|
+
| `secret recipients <add\|remove\|list> [age1…]` | Manage `.sops.yaml` recipients + re-encrypt |
|
|
356
|
+
| `secret check` | Static lint — flag plaintext secret leaks |
|
|
357
|
+
|
|
358
|
+
Options: `-e, --env <env>` (`local` default; also `development`/`staging`/`production`),
|
|
359
|
+
`-p, --package <pkg>` (schema source, default `@spfn/core`).
|
|
360
|
+
|
|
361
|
+
**Local (keychain).** `spfn secret set DB_URL` stores the value in the OS keychain
|
|
362
|
+
(macOS `security`, Windows Credential Manager via optional `@napi-rs/keyring`, Linux
|
|
363
|
+
libsecret) and writes a `secret:keychain:spfn_DB_URL` reference into `.env.server`. The
|
|
364
|
+
reference is not sensitive; the real value never lands in the repo. `spfn dev` resolves
|
|
365
|
+
and injects it. Note: injection happens only when the server is started via `spfn dev` —
|
|
366
|
+
running the app another way (a bare `node`, tests) would see the raw reference, so use
|
|
367
|
+
`spfn dev` locally (a runtime resolver for other runners is planned).
|
|
368
|
+
|
|
369
|
+
**Deployed (SOPS).** `spfn secret set DB_URL --env production` writes the value into
|
|
370
|
+
`secrets/production.enc.json`, encrypted by SOPS. The backend (age / GCP KMS / AWS KMS)
|
|
371
|
+
is chosen by `.sops.yaml` creation rules — KMS needs no local key file (IAM + cloud
|
|
372
|
+
auth), age is the no-cloud fallback (`secret keygen` + `secret recipients add`). Commit
|
|
373
|
+
the encrypted file; your GitOps step decrypts it into env at deploy time. `sops`/`age`
|
|
374
|
+
are needed only for the deployed envs, never for local keychain use.
|
|
375
|
+
|
|
376
|
+
Schema-driven: a secret declared with `envSecret({ generate: 'base64url32' })` can be
|
|
377
|
+
minted/rotated automatically (`secret generate`/`rotate`); one without `generate` is an
|
|
378
|
+
external value you paste in (`secret set`).
|
|
379
|
+
|
|
380
|
+
### `spfn setup icons`
|
|
381
|
+
|
|
382
|
+
Install and configure SVGR for SVG-as-component imports (Next.js only).
|
|
383
|
+
|
|
384
|
+
### `spfn ops`
|
|
385
|
+
|
|
386
|
+
Invoke a running app's ops surface — the routes it exposes with
|
|
387
|
+
[`createOpsRouter`](../core/README.md#how-do-i-operate-the-app-from-the-terminal).
|
|
388
|
+
Commands are discovered from the app's `GET /_ops/_manifest`, so nothing is generated or
|
|
389
|
+
configured locally.
|
|
121
390
|
|
|
122
|
-
**Example: Installing @spfn/cms**
|
|
123
391
|
```bash
|
|
124
|
-
|
|
392
|
+
spfn ops list --app https://api.example.com # what can this app do?
|
|
393
|
+
spfn ops call listSignups --query limit=50 # invoke a command
|
|
394
|
+
spfn ops call refundOrder --param id=42 --data '{"reason":"duplicate"}'
|
|
395
|
+
spfn ops call listSignups --describe # what does it take?
|
|
396
|
+
```
|
|
125
397
|
|
|
126
|
-
|
|
127
|
-
|
|
398
|
+
`--describe` answers from the manifest's schemas, so the usage is the running app's own,
|
|
399
|
+
not a local copy that can drift:
|
|
128
400
|
|
|
129
|
-
|
|
130
|
-
|
|
401
|
+
```
|
|
402
|
+
listSignups GET /_ops/signups
|
|
131
403
|
|
|
132
|
-
|
|
404
|
+
query parameters (--query)
|
|
405
|
+
limit number optional 1–100, default 10
|
|
406
|
+
state string required one of: pending, approved
|
|
133
407
|
|
|
134
|
-
|
|
135
|
-
1. Import CMS components: import { useLabels } from '@spfn/cms'
|
|
136
|
-
2. View labels in Drizzle Studio: pnpm spfn db studio
|
|
137
|
-
3. Learn more: https://github.com/spfnio/spfn
|
|
408
|
+
Invoke: spfn ops call listSignups --query limit=<value>
|
|
138
409
|
```
|
|
139
410
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
- Packages include pre-built migrations in their `migrations/` directory
|
|
153
|
-
- Package migrations are applied first, then project migrations
|
|
154
|
-
- Works with both published npm packages and local development (workspace packages)
|
|
155
|
-
|
|
156
|
-
### Development & Production
|
|
411
|
+
Add `--json` for the raw JSON Schema. The server still validates every call — `--describe`
|
|
412
|
+
reports what it will accept, and the app's answer decides.
|
|
413
|
+
|
|
414
|
+
The app URL comes from `--app` or `SPFN_OPS_APP`, and it must be **https** — every one of
|
|
415
|
+
these commands carries a secret, and `token issue` carries an administrator's password.
|
|
416
|
+
`http` is accepted only against `localhost`, `127.0.0.1` and `::1`, where there is no
|
|
417
|
+
network to listen on. A URL with a base path (`https://example.com/api`) is kept whole:
|
|
418
|
+
both the ops calls and the administrator sign-in go through it.
|
|
419
|
+
|
|
420
|
+
The ops token resolves `--token` → `SPFN_OPS_TOKEN` → macOS keychain, and its lifecycle is
|
|
421
|
+
managed with:
|
|
422
|
+
|
|
157
423
|
```bash
|
|
158
|
-
|
|
159
|
-
spfn
|
|
160
|
-
spfn
|
|
161
|
-
spfn
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
spfn build # Build Next.js + compile server
|
|
165
|
-
spfn start # Start production server
|
|
166
|
-
spfn start --server-only # Start API server only (no Next.js)
|
|
424
|
+
spfn ops token issue --name laptop --scopes 'waitlist:read' --app <url>
|
|
425
|
+
spfn ops token issue --name ci --scopes '*' --no-expiry --to-keychain --app <url>
|
|
426
|
+
spfn ops token list --app <url>
|
|
427
|
+
spfn ops token revoke <id> --app <url>
|
|
428
|
+
spfn ops token store --app <url> # hidden prompt → keychain
|
|
429
|
+
spfn ops token forget --app <url>
|
|
167
430
|
```
|
|
168
431
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
432
|
+
`issue`, `list` and `revoke` call the app's own admin-only routes, so the CLI prompts for
|
|
433
|
+
an administrator's email and password first — no database access. Only the token's SHA-256
|
|
434
|
+
hash is ever stored, so the secret exists in the clear once, in the issuance answer;
|
|
435
|
+
`--to-keychain` delivers it without printing it.
|
|
436
|
+
|
|
437
|
+
`--expires-days` takes 1 to 36500 days (about a century), or `--no-expiry` for a token that
|
|
438
|
+
never expires. The upper bound is there because a day count becomes a date by arithmetic,
|
|
439
|
+
and a big enough count produces an invalid date rather than a distant one.
|
|
440
|
+
|
|
441
|
+
SPFN authenticates a request with a JWT the client signs itself, so the CLI generates a key
|
|
442
|
+
pair for the command, signs the one call it needs, and revokes the key before the command
|
|
443
|
+
ends — on the failing path as much as the succeeding one. Nothing is written to disk.
|
|
444
|
+
|
|
445
|
+
These three commands need `@spfn/auth` **0.3.0-beta.2 or later** installed in the app: the
|
|
446
|
+
ops token lives in its schema, and the signing comes from its `@spfn/auth/crypto` entry
|
|
447
|
+
point, which that release added. The CLI does not depend on the package in any form — it
|
|
448
|
+
loads it from the app at run time, and tells a missing package apart from one too old to
|
|
449
|
+
carry the entry point, so the message names the thing to do.
|
|
450
|
+
|
|
451
|
+
Because the package is the app's, it is resolved **from the directory the command runs in**.
|
|
452
|
+
Run `spfn ops token` from the app's root; running it elsewhere reports the package as
|
|
453
|
+
missing, and the message names the directory it looked in.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Scaffold structure
|
|
458
|
+
|
|
459
|
+
Both modes produce the core full-stack skeleton:
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
src/
|
|
463
|
+
app/api/rpc/[routeName]/route.ts # RPC proxy — re-exports { GET, POST } from @spfn/core/nextjs/server
|
|
464
|
+
generated/route-map.ts # generated by codegen (run `spfn codegen run` if missing)
|
|
465
|
+
lib/
|
|
466
|
+
api-client.ts # createApi<AppRouter>() — the type-safe client
|
|
467
|
+
server/
|
|
468
|
+
router.ts # defineRouter({ ...routes }) → export type AppRouter
|
|
469
|
+
server.config.ts # defineServerConfig().port(8790).host('0.0.0.0').routes(appRouter)
|
|
470
|
+
config/env.config.ts # environment schema
|
|
471
|
+
entities/ # Drizzle tables (example.entity.ts, config.ts)
|
|
472
|
+
repositories/ # BaseRepository subclasses (example.repository.ts)
|
|
473
|
+
routes/ # route DSL handlers: root.ts, health.ts, examples.ts
|
|
474
|
+
tsconfig.json, tsup.config.ts
|
|
475
|
+
.spfnrc.ts # codegen config (route-map generator)
|
|
476
|
+
spfn.config.js # deployment config (subdomain/region/domains) — committed
|
|
477
|
+
docker-compose.yml # Postgres + Redis (dev)
|
|
478
|
+
docker-compose.production.yml
|
|
479
|
+
Dockerfile, .dockerignore
|
|
480
|
+
next.config.ts # patched when auth is enabled: /_auth/:path* rewrite → SPFN API
|
|
481
|
+
.env.example # committed reference — every key, placeholder values
|
|
482
|
+
.env.local # generated, gitignored (values loaded by Next.js)
|
|
483
|
+
.env.server # generated, gitignored (server secrets: DB, cache)
|
|
177
484
|
```
|
|
178
485
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
486
|
+
Full mode overlays the Prototype-to-Production baseline:
|
|
487
|
+
|
|
488
|
+
```
|
|
489
|
+
src/
|
|
490
|
+
app/login/page.tsx # provider login starter UI
|
|
491
|
+
app/auth/callback/page.tsx # OAuth session handoff
|
|
492
|
+
i18n/catalogs.ts # application-owned en/ko starter messages
|
|
493
|
+
i18n/server.ts # configured server-side i18n registry
|
|
494
|
+
server/mcp.ts # authenticated /mcp endpoint + starter app_status tool
|
|
495
|
+
server/router.ts # authRouter + mcpRouter + global authenticate
|
|
496
|
+
server/server.config.ts # createAuthLifecycle + i18n startup
|
|
497
|
+
next.config.ts # /_auth/* callback rewrite
|
|
498
|
+
.env.local # generated auth session secret (gitignored)
|
|
499
|
+
.env.server # auth keyring + MCP operator key (gitignored)
|
|
182
500
|
```
|
|
183
501
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
502
|
+
The full RPC proxy imports the auth interceptor and merges `authRouteMap`. Internal auth
|
|
503
|
+
and MCP keys are generated with cryptographic randomness in ignored local env files;
|
|
504
|
+
`.env.example` contains placeholders only. Add only the provider keys you use, then run
|
|
505
|
+
`pnpm spfn db migrate`. The starter MCP endpoint accepts `SPFN_MCP_API_KEY` as a Bearer
|
|
506
|
+
token for first-party operation; replace that validator with OAuth before third-party access.
|
|
507
|
+
|
|
508
|
+
`init` also patches `package.json` (scripts: `spfn:dev`, `spfn:server`, `spfn:next`,
|
|
509
|
+
`spfn:build`, `spfn:start`, `codegen`; deps: `@spfn/core`, `spfn`, `drizzle-orm`,
|
|
510
|
+
`@sinclair/typebox`, `concurrently`, etc.; full also adds `@spfn/auth`, `@spfn/i18n`,
|
|
511
|
+
`@spfn/mcp`, auth's `@spfn/notification` peer, and a Node `>=20.0.0` engine when the
|
|
512
|
+
existing range still permits older Node versions), excludes `src/server` from the root
|
|
513
|
+
`tsconfig.json` (Vercel compat), and adds `.spfn/`, `.env.local`, `.env.server` to
|
|
514
|
+
`.gitignore`.
|
|
515
|
+
|
|
516
|
+
### Route DSL (the current architecture)
|
|
517
|
+
|
|
518
|
+
Routes are defined with the `route` builder and collected by `defineRouter`. There is no
|
|
519
|
+
separate "contract" layer — the router's type *is* the contract; the client infers from it.
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
// src/server/routes/examples.ts
|
|
523
|
+
import { route } from '@spfn/core/route';
|
|
524
|
+
import { Type } from '@sinclair/typebox';
|
|
525
|
+
|
|
526
|
+
export const getExample = route.get('/examples/:id')
|
|
527
|
+
.input({ params: Type.Object({ id: Type.String() }) })
|
|
528
|
+
.handler(async (c) =>
|
|
529
|
+
{
|
|
530
|
+
const { params } = await c.data();
|
|
531
|
+
return { id: params.id };
|
|
532
|
+
});
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
// src/server/router.ts
|
|
537
|
+
import { defineRouter } from '@spfn/core/route';
|
|
538
|
+
import { getExample } from './routes/examples';
|
|
539
|
+
|
|
540
|
+
export const appRouter = defineRouter({ getExample });
|
|
541
|
+
export type AppRouter = typeof appRouter;
|
|
187
542
|
```
|
|
188
543
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
```javascript
|
|
197
|
-
/**
|
|
198
|
-
* @type {import('spfn').SpfnConfig}
|
|
199
|
-
*/
|
|
200
|
-
export default {
|
|
201
|
-
packageManager: 'pnpm',
|
|
202
|
-
deployment: {
|
|
203
|
-
// Your app's subdomain on spfn.app
|
|
204
|
-
// Creates region-specific domains:
|
|
205
|
-
// - myapp.us.spfn.app (Next.js)
|
|
206
|
-
// - api-myapp.us.spfn.app (API)
|
|
207
|
-
subdomain: 'myapp',
|
|
208
|
-
|
|
209
|
-
// Optional: Deployment region (defaults to 'us')
|
|
210
|
-
// Available: 'us' (Virginia, default), 'kr' (Seoul), 'jp', 'sg', 'eu' (coming soon)
|
|
211
|
-
region: 'us',
|
|
212
|
-
|
|
213
|
-
// Optional: Add custom domains
|
|
214
|
-
customDomains: {
|
|
215
|
-
nextjs: ['www.example.com', 'example.com'],
|
|
216
|
-
spfn: ['api.example.com']
|
|
217
|
-
},
|
|
218
|
-
|
|
219
|
-
// Optional: Environment variables for both Next.js and SPFN backend
|
|
220
|
-
// ⚠️ WARNING: These values are committed to Git
|
|
221
|
-
// Do NOT put sensitive credentials here!
|
|
222
|
-
env: {
|
|
223
|
-
NEXT_PUBLIC_API_URL: 'https://api-myapp.us.spfn.app',
|
|
224
|
-
NODE_ENV: 'production'
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
544
|
+
```ts
|
|
545
|
+
// src/lib/api-client.ts
|
|
546
|
+
import { createApi } from '@spfn/core/nextjs';
|
|
547
|
+
import type { AppRouter } from '@/server/router';
|
|
548
|
+
|
|
549
|
+
export const api = createApi<AppRouter>();
|
|
550
|
+
const example = await api.getExample.call({ params: { id: '123' } });
|
|
228
551
|
```
|
|
229
552
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
-
|
|
233
|
-
- **Dual Domain Setup** - Automatic `{subdomain}.{region}.spfn.app` and `api-{subdomain}.{region}.spfn.app`
|
|
234
|
-
- **Custom Domains** - Support for multiple custom domains
|
|
235
|
-
- **Environment Variables** - Shared between Next.js and SPFN backend
|
|
236
|
-
- **ESM/CJS Support** - Works with both module systems
|
|
553
|
+
Client calls go through the Next.js RPC proxy (`/api/rpc/[routeName]`), which forwards to
|
|
554
|
+
the SPFN API with cookie forwarding and interceptors, resolving routes via the generated
|
|
555
|
+
`route-map.ts`.
|
|
237
556
|
|
|
238
|
-
|
|
239
|
-
- `spfn.config.js` is committed to Git
|
|
240
|
-
- Only use for non-sensitive configuration
|
|
241
|
-
- For secrets (DB passwords, API keys), use CI/CD secrets management
|
|
557
|
+
---
|
|
242
558
|
|
|
243
|
-
##
|
|
559
|
+
## Deployment
|
|
244
560
|
|
|
245
|
-
|
|
246
|
-
- **[SPFN Framework](../../README.md)** - Getting started
|
|
247
|
-
- **[@spfn/core](../core/README.md)** - API reference and core concepts
|
|
561
|
+
Two targets, both shipping from the same repository.
|
|
248
562
|
|
|
249
|
-
|
|
563
|
+
**Vercel — serverless, one origin, no container.** Run `spfn add vercel` (above) and
|
|
564
|
+
deploy. Frontend and backend share a single Vercel origin. One caveat: the in-process job
|
|
565
|
+
worker does not run there, so enqueuing works but nothing drains the queue — schedule a
|
|
566
|
+
route that processes a batch (Vercel Cron), or run jobs on an always-on target.
|
|
250
567
|
|
|
251
|
-
-
|
|
252
|
-
|
|
253
|
-
|
|
568
|
+
**Always-on — a long-lived process.** `spfn build` then `spfn start`, or the generated
|
|
569
|
+
Docker files. Background jobs, WebSocket events and the periodic database health check all
|
|
570
|
+
need this path.
|
|
254
571
|
|
|
255
|
-
|
|
572
|
+
```bash
|
|
573
|
+
# Build + run locally
|
|
574
|
+
pnpm spfn:build
|
|
575
|
+
pnpm spfn:start # Next.js :3790 + SPFN API :8790
|
|
576
|
+
|
|
577
|
+
# Docker (single image runs both)
|
|
578
|
+
docker compose -f docker-compose.production.yml up --build -d
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
The Dockerfile (`node:22-alpine`) installs with `pnpm --frozen-lockfile`, runs
|
|
582
|
+
`pnpm run spfn:build`, prunes dev deps, exposes `3790`/`8790`, health-checks
|
|
583
|
+
`http://localhost:8790/health`, and starts via `pnpm run spfn:start`.
|
|
584
|
+
|
|
585
|
+
Run migrations against the target DB before/with deploy:
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
docker exec <container> npx spfn db migrate
|
|
589
|
+
```
|
|
256
590
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
591
|
+
Forget, and the container will not come up: the server refuses to serve while migrations
|
|
592
|
+
are pending and prints which ones. That is the intended failure — a deploy that stops at
|
|
593
|
+
the gate is one that never served the 500s. If a rollout has to proceed anyway, set
|
|
594
|
+
`SPFN_ALLOW_PENDING_MIGRATIONS=true` in the container's environment; the pending list is
|
|
595
|
+
logged as a warning instead.
|
|
596
|
+
|
|
597
|
+
A readiness probe can catch the same drift on a cluster the local gate never sees. When
|
|
598
|
+
detailed health is on, `GET /health` carries a `migrations` object with per-package
|
|
599
|
+
applied/pending counts — assert `migrations.pending === 0` in the probe to hold a
|
|
600
|
+
drifted pod out of rotation. Reporting drift does not, by itself, change the overall
|
|
601
|
+
health `status`.
|
|
602
|
+
|
|
603
|
+
`spfn.config.js` (committed) configures the managed `*.spfn.app` deployment: `subdomain`,
|
|
604
|
+
`region` (`us` default, `kr`, …), `customDomains`, and non-secret `env`. Its `SpfnConfig`
|
|
605
|
+
type ships from `spfn` (`@type {import('spfn').SpfnConfig}`).
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## FAQ
|
|
610
|
+
|
|
611
|
+
**`create` or `init` — which one?**
|
|
612
|
+
`create` starts a new project: it runs `create-next-app` with SPFN's flags and then runs
|
|
613
|
+
`init` inside it. `init` adds SPFN to a Next.js app that already exists. If you built
|
|
614
|
+
something with an AI coding agent and now want a real backend under it, `init` is the one.
|
|
615
|
+
|
|
616
|
+
**`bare` or `full`?**
|
|
617
|
+
`full` is the recommended baseline: core, auth, i18n and MCP wired together, so you get a
|
|
618
|
+
working authenticated app on day one. `bare` is core only — the architecture with nothing
|
|
619
|
+
else decided. Automation should always pass `--mode` explicitly, because a `--yes` run
|
|
620
|
+
without one still produces `bare` for backward compatibility.
|
|
621
|
+
|
|
622
|
+
**Why doesn't my server restart when I edit a file?**
|
|
623
|
+
Because hot reload is off by default. `spfn dev --watch` restarts the server on `src/server`
|
|
624
|
+
changes. Next.js reloads on its own either way.
|
|
625
|
+
|
|
626
|
+
**Do I have to use Docker?**
|
|
627
|
+
No. Vercel is a first-class target and needs no container. Docker is the always-on path,
|
|
628
|
+
and `docker compose up -d` is also the convenient way to get PostgreSQL and Redis locally —
|
|
629
|
+
pointing at your own PostgreSQL works too. PostgreSQL itself is not optional.
|
|
630
|
+
|
|
631
|
+
**Which Node version do I need?**
|
|
632
|
+
20 or later, in both modes. `@spfn/core` runs on `@hono/node-server` 2, which declares
|
|
633
|
+
that floor, and full mode's MCP server needs the same.
|
|
634
|
+
|
|
635
|
+
**When do I have to run codegen by hand?**
|
|
636
|
+
Whenever routes change outside `spfn dev`, which runs a codegen watcher for you. A stale or
|
|
637
|
+
missing `src/generated/route-map.ts` makes the RPC proxy answer 404 — run `spfn codegen run`
|
|
638
|
+
and commit the result.
|
|
639
|
+
|
|
640
|
+
**Where do secrets go?**
|
|
641
|
+
`.env.server` (gitignored, server-only) for backend values, `.env.local` for the session
|
|
642
|
+
cookie secret that the Next.js runtime itself needs. Never `spfn.config.js` — that file is
|
|
643
|
+
committed.
|
|
644
|
+
|
|
645
|
+
## Pitfalls
|
|
646
|
+
|
|
647
|
+
- **`.env.server` is gitignored and server-only.** Put backend-only DB/secret values there,
|
|
648
|
+
not in `.env` (committed). Full mode's session-cookie secret is the intentional exception:
|
|
649
|
+
it lives in gitignored `.env.local` because Next.js must encrypt the cookie. There is no
|
|
650
|
+
`.env.server.local`. `spfn init` generates `.env.server`; put DB/secret values there.
|
|
651
|
+
Load order is the standard dotenv chain ending with `.env.server`.
|
|
652
|
+
- **Never commit secrets in `spfn.config.js`.** It's checked into Git; its `env` block is
|
|
653
|
+
for non-sensitive values only. Use CI/CD secret management for credentials.
|
|
654
|
+
- **`spfn dev` does not hot-reload by default.** Add `--watch` to restart on `src/server`
|
|
655
|
+
changes.
|
|
656
|
+
- **`spfn start` needs a prior `spfn build`.** It hard-fails without `.spfn/server`,
|
|
657
|
+
`.spfn/prod-server.mjs`, and `.next`.
|
|
658
|
+
- **Destructive DB commands are guarded.** `db drop` double-confirms (and verifies the
|
|
659
|
+
target); `db push` applies the selected statements in one transaction and withholds
|
|
660
|
+
destructive ones unless `--force`/confirmed. Prefer `db generate` + `db migrate` for
|
|
661
|
+
production; `db push` is dev-only.
|
|
662
|
+
- **`spfn add` requires a scoped package name** (must contain `/`) and only applies
|
|
663
|
+
migrations when `DATABASE_URL` is set — otherwise it skips with a hint. The single
|
|
664
|
+
exception is `spfn add vercel`, a built-in target rather than a package.
|
|
665
|
+
- **Package manager is auto-detected from lockfiles.** If detection is wrong (e.g. mixed
|
|
666
|
+
lockfiles), pass `--pm` to `create`. In a pnpm workspace, `create` installs from the
|
|
667
|
+
workspace root, not the new project dir.
|
|
668
|
+
- **Make scaffold mode explicit in automation.** Interactive runs recommend `full`, while
|
|
669
|
+
historical `--yes` calls without `--mode` remain `bare`. Pass `--mode full` or
|
|
670
|
+
`--mode bare` so scripts state their intended architecture.
|
|
671
|
+
- **Regenerate the route map after route changes outside dev.** If
|
|
672
|
+
`src/generated/route-map.ts` is missing or stale, run `spfn codegen run` — the RPC proxy
|
|
673
|
+
depends on it.
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
## Related
|
|
678
|
+
|
|
679
|
+
- [`@spfn/core`](../core/README.md) — server, route DSL, codegen, db, client runtime.
|
|
680
|
+
- [`@spfn/auth`](../auth/README.md) — what `--mode full` wires in for accounts and roles.
|
|
681
|
+
- [`@spfn/mcp`](../mcp/README.md) — what `--mode full` wires in for operating the app.
|
|
682
|
+
- Project root README — framework overview and getting started.
|