spfn 0.3.0-beta.7 → 0.3.0-beta.8

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/README.md CHANGED
@@ -703,11 +703,25 @@ docker-compose.yml # Postgres + Redis (dev)
703
703
  docker-compose.production.yml
704
704
  Dockerfile, .dockerignore
705
705
  next.config.ts # patched when auth is enabled: /_auth/:path* rewrite → SPFN API
706
- .env.example # committed reference — every key, placeholder values
706
+ .env.local.example # committed reference — Next.js keys, placeholder values
707
+ .env.server.example # committed reference — backend keys, placeholder values
707
708
  .env.local # generated, gitignored (values loaded by Next.js)
708
709
  .env.server # generated, gitignored (server secrets: DB, cache)
709
710
  ```
710
711
 
712
+ The reference is split by consumer, never combined, and a key's file follows who
713
+ reads it rather than whether it is secret:
714
+
715
+ - `.env.local` / `.env.local.example` — the Next.js process (server routes, proxy,
716
+ SSR) and, through `NEXT_PUBLIC_*`, the browser: `SPFN_API_URL`, `SPFN_APP_URL`,
717
+ `SPFN_AUTH_SESSION_SECRET`, `SPFN_AUTH_SESSION_TTL`, `SPFN_AUTH_CSRF`.
718
+ - `.env.server` / `.env.server.example` — the SPFN backend only, never loaded by
719
+ Next.js: `NODE_ENV`, `SPFN_LOG_LEVEL`, `DATABASE_URL`, `CACHE_URL`, `DB_POOL_*`,
720
+ and every OAuth, token, and admin secret.
721
+
722
+ So the session secret is secret and still lives in `.env.local` — the Next.js proxy
723
+ verifies cookies with it — while `DATABASE_URL` never appears there at all.
724
+
711
725
  Full mode overlays the Prototype-to-Production baseline:
712
726
 
713
727
  ```
@@ -726,7 +740,7 @@ next.config.ts # /_auth/* callback rewrite
726
740
 
727
741
  The full RPC proxy imports the auth interceptor and merges `authRouteMap`. Internal auth
728
742
  keys are generated with cryptographic randomness in ignored local env files;
729
- `.env.example` contains placeholders only. Add only the provider keys you use, then run
743
+ `.env.local.example` and `.env.server.example` contain placeholders only. Add only the provider keys you use, then run
730
744
  `pnpm spfn db migrate`.
731
745
 
732
746
  Operating the app is [`spfn ops`](#spfn-ops), not a dashboard: the starter
package/dist/index.js CHANGED
@@ -948,7 +948,7 @@ import { dirname as dirname2, join as join10 } from "path";
948
948
  import { fileURLToPath as fileURLToPath2 } from "url";
949
949
  function getCliVersion() {
950
950
  if (true) {
951
- return "0.3.0-beta.7";
951
+ return "0.3.0-beta.8";
952
952
  }
953
953
  return fromPackageJson ?? (fromPackageJson = readPackageVersion());
954
954
  }
@@ -1169,14 +1169,6 @@ import { randomBytes } from "crypto";
1169
1169
  import { spawnSync } from "child_process";
1170
1170
  import { join as join12 } from "path";
1171
1171
  import fse8 from "fs-extra";
1172
- function envExampleTemplate(mode) {
1173
- return withPlaceholderCreds(`# Example environment \u2014 committed reference for the variables SPFN uses.
1174
- # Real values live in .env.local (Next.js) and .env.server (backend secrets),
1175
- # both gitignored. This file documents the keys; it is not loaded by anything.
1176
-
1177
- ${ENV_LOCAL_TEMPLATE}${mode === "full" ? FULL_ENV_LOCAL_EXAMPLE : ""}
1178
- ${ENV_SERVER_TEMPLATE}${mode === "full" ? FULL_ENV_SERVER_EXAMPLE : ""}`);
1179
- }
1180
1172
  function envServerExampleTemplate(mode) {
1181
1173
  return withPlaceholderCreds(
1182
1174
  `${ENV_SERVER_TEMPLATE}${mode === "full" ? FULL_ENV_SERVER_EXAMPLE : ""}`
@@ -1195,7 +1187,9 @@ NEXT_PUBLIC_SPFN_APP_URL=http://localhost:3790
1195
1187
  `;
1196
1188
  }
1197
1189
  function envLocalExampleTemplate(mode) {
1198
- return `${ENV_LOCAL_TEMPLATE}${mode === "full" ? FULL_ENV_LOCAL_EXAMPLE : ""}`;
1190
+ return withPlaceholderCreds(
1191
+ `${ENV_LOCAL_TEMPLATE}${mode === "full" ? FULL_ENV_LOCAL_EXAMPLE : ""}`
1192
+ );
1199
1193
  }
1200
1194
  function envServerTemplate(mode) {
1201
1195
  if (mode === "bare") {
@@ -1261,20 +1255,16 @@ export default defineConfig({
1261
1255
  updateTsconfig(cwd);
1262
1256
  }
1263
1257
  function generateEnvFiles(cwd, mode) {
1264
- writeLocalEnv(cwd, envLocalTemplate(mode), envLocalExampleTemplate(mode));
1265
- writeServerEnv(cwd, envServerTemplate(mode), envServerExampleTemplate(mode));
1266
- writeExampleEnv(cwd, envExampleTemplate(mode));
1258
+ writeReferenceEnv(cwd, ".env.local.example", envLocalExampleTemplate(mode));
1259
+ writeReferenceEnv(cwd, ".env.server.example", envServerExampleTemplate(mode));
1260
+ writeLocalEnv(cwd, envLocalTemplate(mode));
1261
+ writeServerEnv(cwd, envServerTemplate(mode));
1267
1262
  }
1268
- function writeLocalEnv(cwd, template, exampleTemplate) {
1263
+ function writeLocalEnv(cwd, template) {
1269
1264
  const filename = ".env.local";
1270
1265
  const filePath = join12(cwd, filename);
1271
1266
  if (isGitTracked(cwd, filename)) {
1272
- const referenceName = ".env.local.spfn.example";
1273
- const referencePath = join12(cwd, referenceName);
1274
- if (!existsSync12(referencePath)) {
1275
- writeFileSync8(referencePath, exampleTemplate);
1276
- }
1277
- logger.warn(`${filename} is tracked by Git \u2014 left it untouched; wrote ${referenceName}. Untrack ${filename}, then add the missing keys with freshly generated values`);
1267
+ logger.warn(`${filename} is tracked by Git \u2014 left it untouched; its keys are listed in .env.local.example. Untrack ${filename}, then add the missing keys with freshly generated values`);
1278
1268
  return;
1279
1269
  }
1280
1270
  writeEnvFile(cwd, filename, template);
@@ -1287,7 +1277,7 @@ function isGitTracked(cwd, filename) {
1287
1277
  });
1288
1278
  return result.status === 0;
1289
1279
  }
1290
- function writeServerEnv(cwd, template, exampleTemplate) {
1280
+ function writeServerEnv(cwd, template) {
1291
1281
  const filePath = join12(cwd, ".env.server");
1292
1282
  if (!existsSync12(filePath)) {
1293
1283
  writeFileSync8(filePath, template);
@@ -1295,16 +1285,15 @@ function writeServerEnv(cwd, template, exampleTemplate) {
1295
1285
  logger.success("Created .env.server");
1296
1286
  return;
1297
1287
  }
1298
- writeFileSync8(join12(cwd, ".env.server.example"), exampleTemplate);
1299
- logger.warn(".env.server already exists \u2014 left it untouched; wrote SPFN's reference to .env.server.example, add any missing keys manually");
1288
+ logger.warn(".env.server already exists \u2014 left it untouched; SPFN's reference is in .env.server.example, add any missing keys manually");
1300
1289
  }
1301
- function writeExampleEnv(cwd, content) {
1302
- const filePath = join12(cwd, ".env.example");
1290
+ function writeReferenceEnv(cwd, filename, content) {
1291
+ const filePath = join12(cwd, filename);
1303
1292
  if (existsSync12(filePath)) {
1304
1293
  return;
1305
1294
  }
1306
1295
  writeFileSync8(filePath, content);
1307
- logger.success("Created .env.example (committed reference)");
1296
+ logger.success(`Created ${filename} (committed reference)`);
1308
1297
  }
1309
1298
  function writeEnvFile(cwd, filename, content) {
1310
1299
  const filePath = join12(cwd, filename);
@@ -1362,8 +1351,11 @@ function pendingIgnoreRules(content) {
1362
1351
  if (!gitignoreCovers(lines, ".env.server")) {
1363
1352
  rules.push("\n# spfn server env (secrets)\n.env.server\n");
1364
1353
  }
1365
- if (!lines.some((line) => line.trim() === "!.env.example")) {
1366
- rules.push("\n# spfn env reference (committed)\n!.env.example\n");
1354
+ if (!lines.some((line) => line.trim() === "!.env.local.example")) {
1355
+ rules.push("\n# spfn env reference, Next.js keys (committed)\n!.env.local.example\n");
1356
+ }
1357
+ if (!lines.some((line) => line.trim() === "!.env.server.example")) {
1358
+ rules.push("\n# spfn env reference, backend keys (committed)\n!.env.server.example\n");
1367
1359
  }
1368
1360
  return rules;
1369
1361
  }
@@ -7148,7 +7140,7 @@ init_logger();
7148
7140
  import { join as join32 } from "path";
7149
7141
  import chalk43 from "chalk";
7150
7142
  init_env_file();
7151
- var COMMITTED_FILES = [".env", ".env.example"];
7143
+ var COMMITTED_FILES = [".env", ".env.example", ".env.local.example", ".env.server.example"];
7152
7144
  var PLACEHOLDER = /(your-|changeme|placeholder|example|<.*>)/i;
7153
7145
  async function secretCheck(options) {
7154
7146
  const cwd = process.cwd();
@@ -32,7 +32,8 @@ src/
32
32
  generated/ codegen output (route map) — do not edit by hand
33
33
  .env.local Next.js runtime env (gitignored)
34
34
  .env.server server secrets (gitignored)
35
- .env.example committed reference — keys only, placeholder values
35
+ .env.local.example committed reference — the Next.js keys, placeholder values
36
+ .env.server.example committed reference — the backend keys, placeholder values
36
37
  .spfnrc.ts codegen configuration
37
38
  spfn.config.js deployment config
38
39
  ```
@@ -54,6 +55,12 @@ A feature is a vertical slice: `Entity` (Drizzle table) → `Repository` →
54
55
 
55
56
  ## Environment & secrets
56
57
 
58
+ The env reference is split by consumer: `.env.local.example` lists what the Next.js
59
+ process reads (`SPFN_API_URL`, `NEXT_PUBLIC_*`, the auth session secret), and
60
+ `.env.server.example` lists what only the backend reads (`DATABASE_URL`, `CACHE_URL`,
61
+ OAuth secrets). Which file a key belongs in follows who consumes it, not whether it
62
+ is secret.
63
+
57
64
  `.env.server` holds backend-only secrets and is gitignored — Next.js never loads it.
58
65
  For a managed workflow use `spfn secret`: local values go to the OS keychain (only a
59
66
  `secret:keychain:` reference lands in `.env.server`), and deployed secrets are stored
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spfn",
3
- "version": "0.3.0-beta.7",
3
+ "version": "0.3.0-beta.8",
4
4
  "description": "Scaffold a full-stack TypeScript backend onto a Next.js app built with an AI coding agent: auth, database, typed routes and codegen, one fixed vertical slice per feature",
5
5
  "type": "module",
6
6
  "bin": {