create-meith 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -176,25 +176,24 @@ function vercelEnvExample(name) {
176
176
  # no other instance can read and that is discarded with the instance \u2014 on Vercel
177
177
  # the board refuses it outright rather than losing uploads quietly.
178
178
  #
179
- # CACHE_DRIVER and FILESTORE_DRIVER are asked for at deploy time. The other
180
- # three derive themselves: a DATABASE_URL means postgres for the data source and
181
- # the queue, and a RESEND_API_KEY with a MAIL_FROM beside it means mail over the
182
- # provider's HTTPS API. Setting one here overrides the derivation, which is what
183
- # this file is for when you copy it to .env.local.
179
+ # The deploy form asks for none of them. On Vercel the board works each one out
180
+ # from what the linked stores publish: a DATABASE_URL means postgres for the
181
+ # data source and the queue, a Redis connection string means CACHE_DRIVER=redis,
182
+ # a Blob store's read-write token means FILESTORE_DRIVER=blob, and a
183
+ # RESEND_API_KEY with a MAIL_FROM beside it means mail over the provider's HTTPS
184
+ # API. Setting one here overrides the derivation, which is what this file is for
185
+ # when you copy it to .env.local.
184
186
  #
185
- # The two that stay explicit stay that way on purpose: each turns on an external
186
- # service the board then depends on for every request, and neither is switched on
187
- # by a variable that merely happens to be present. The board caches in Redis and
188
- # puts uploads in an object store because you said so.
187
+ # Every one of those derivations is scoped to Vercel, and each fires only from a
188
+ # value that is unambiguously the thing itself \u2014 a redis:// or rediss:// URL, a
189
+ # read-write token. A board you run anywhere else is untouched by all of it and
190
+ # still takes these values from this file, exactly as it did before.
189
191
  #
190
- # What happens if you leave one blank differs, and the difference is the reason
191
- # the form asks for both. Leave FILESTORE_DRIVER blank and the board refuses to
192
- # boot at all: the default is local, and local on this platform means uploads
193
- # written to a disk that is about to disappear. Leave CACHE_DRIVER blank and
194
- # nothing complains \u2014 the board falls back to caching inside each instance and
195
- # serves whatever that instance last saw, for up to a minute, however many
196
- # instances there are. That one degrades quietly, which is exactly why it is
197
- # worth typing rather than leaving to a default.
192
+ # A derivation that cannot resolve is a configuration error, not an invitation
193
+ # to pick something safe-looking. On Vercel, with the cache or the object store
194
+ # missing, the board refuses to boot and names every variable it looked at. It
195
+ # will not quietly cache inside the instance, and it will not quietly write
196
+ # uploads to a disk that is about to disappear.
198
197
  DATA_SOURCE=postgres
199
198
  QUEUE_DRIVER=postgres
200
199
  CACHE_DRIVER=redis
@@ -208,16 +207,20 @@ DATABASE_URL=
208
207
 
209
208
  ${ENV_DIRECT_DATABASE_URL_PROSE}
210
209
  #
211
- # On Vercel this is not optional. DATABASE_URL here is the pooler string, the
212
- # build runs \`community migrate\` against it, and /install takes the second of
213
- # those two session locks on first run. Neon publishes the direct string as
214
- # DATABASE_URL_UNPOOLED; copy that value into this variable.
210
+ # On Vercel this is not optional, and it is no longer yours to copy. DATABASE_URL
211
+ # here is the pooler string, the build runs \`community migrate\` against it, and
212
+ # /install takes the second of those two session locks on first run. Left blank,
213
+ # the board reads Neon's own direct string \u2014 \`DATABASE_URL_UNPOOLED\` first, then
214
+ # \`POSTGRES_URL_NON_POOLING\` \u2014 and refuses to boot if neither is there, naming
215
+ # both. Never \`POSTGRES_URL\`: that one is pooled.
215
216
  DIRECT_DATABASE_URL=
216
217
 
217
218
  # The shared cache \u2014 a Redis or Valkey endpoint, \`rediss://\` for TLS. Redis
218
219
  # holds cache entries and nothing else: losing it costs the board a warm cache,
219
- # not data, and signs nobody out. A store provisioned with the board publishes
220
- # its own connection variable; copy that value into this one.
220
+ # not data, and signs nobody out. Left blank on Vercel, the board reads the
221
+ # Upstash store's own \`KV_URL\`, which is the one variable it publishes that
222
+ # speaks the Redis protocol \u2014 \`KV_REST_API_URL\` is an HTTPS endpoint and is
223
+ # never used for this. A name we do not know goes here by hand.
221
224
  REDIS_URL=
222
225
 
223
226
  ${ENV_AUTH_SECRET_PROSE}
@@ -233,15 +236,26 @@ ${ENV_TICK_SECRET_PROSE}
233
236
  CRON_SECRET=
234
237
  # TICK_SECRET=
235
238
 
236
- # Uploads, in the Vercel Blob store the Deploy Button provisions. The store
237
- # publishes this variable into the project by itself, so there is nothing to
238
- # type and nothing to mistype \u2014 but the board still needs FILESTORE_DRIVER=blob
239
- # above before it will use it. Every object is written with private access: an
240
- # object URL is not a public link, and member content is served by the board,
241
- # which is where permissions are checked.
239
+ # Uploads, in the Vercel Blob store the Deploy Button provisions. A store
240
+ # attached to the project publishes BLOB_STORE_ID and nothing else \u2014 no token \u2014
241
+ # because the SDK authenticates with the deployment's own OIDC identity: the
242
+ # board hands it the store id and lets it fetch the credential. There is nothing
243
+ # to type and nothing to mistype, and FILESTORE_DRIVER=blob derives from this
244
+ # variable being present.
242
245
  #
243
- # An upload is held whole in the instance's memory on the way in and on the way
244
- # out, so the function's memory limit, not the store, is what caps a file.
246
+ # BLOB_READ_WRITE_TOKEN is the other way in, and you make it yourself on the
247
+ # store. Set it when something has to reach the store from OUTSIDE a Vercel
248
+ # deployment \u2014 \`community backup\` run on your own machine is the case that
249
+ # matters \u2014 because there is no OIDC identity there to borrow. Set both and the
250
+ # board prefers the store id, unless the token names a different store, in which
251
+ # case the token wins: naming another store is a deliberate act.
252
+ #
253
+ # Every object is written with private access: an object URL is not a public
254
+ # link, and member content is served by the board, which is where permissions
255
+ # are checked. An upload is held whole in the instance's memory on the way in
256
+ # and on the way out, so the function's memory limit, not the store, is what
257
+ # caps a file.
258
+ BLOB_STORE_ID=
245
259
  BLOB_READ_WRITE_TOKEN=
246
260
 
247
261
  # Uploads in an S3-compatible bucket instead \u2014 AWS, R2, MinIO, Spaces. This is
@@ -264,10 +278,10 @@ BLOB_READ_WRITE_TOKEN=
264
278
  # board refuses it on Vercel; 587 with STARTTLS may work, but an API does not
265
279
  # depend on the platform's egress rules staying as they are.
266
280
  #
267
- # MAIL_FROM is yours to decide and the one mail value the button asks for: it
268
- # must be an address at a domain the provider has verified for you, and no
269
- # default is right. Send from an unverified sender and the provider rejects
270
- # every message.
281
+ # MAIL_FROM is yours to decide, and it belongs to the step that adds Resend
282
+ # rather than to the deploy: it must be an address at a domain the provider has
283
+ # verified for you, no default is right, and until a provider is added the board
284
+ # cannot send from any address at all. Set it alongside the integration.
271
285
  MAIL_FROM=
272
286
 
273
287
  # Add the Resend integration to the project from Vercel's marketplace and it
@@ -308,21 +322,14 @@ var SELF_HOST_DEPLOY_KIT = [
308
322
  "docker-entrypoint.sh",
309
323
  "docker-healthcheck.sh"
310
324
  ];
311
- var VERCEL_TYPED_DRIVERS = ["CACHE_DRIVER=redis", "FILESTORE_DRIVER=blob"];
312
325
  var VERCEL_DERIVED_DRIVERS = [
313
326
  "DATA_SOURCE=postgres",
314
327
  "QUEUE_DRIVER=postgres",
328
+ "CACHE_DRIVER=redis",
329
+ "FILESTORE_DRIVER=blob",
315
330
  "MAIL_DRIVER=http"
316
331
  ];
317
- var VERCEL_PROMPTED_ENV = [
318
- "CACHE_DRIVER",
319
- "FILESTORE_DRIVER",
320
- "DIRECT_DATABASE_URL",
321
- "REDIS_URL",
322
- "AUTH_SECRET",
323
- "CRON_SECRET",
324
- "MAIL_FROM"
325
- ];
332
+ var VERCEL_PROMPTED_ENV = ["AUTH_SECRET", "CRON_SECRET"];
326
333
  var VERCEL_MARKETPLACE_STORES = [
327
334
  { type: "integration", integrationSlug: "neon", productSlug: "neon", protocol: "storage" },
328
335
  {
@@ -341,7 +348,7 @@ function deployButtonUrl(templateRepositoryUrl) {
341
348
  ["env", VERCEL_PROMPTED_ENV.join(",")],
342
349
  [
343
350
  "envDescription",
344
- "Two driver values, two secrets to generate, the direct database URL, the cache URL, and the verified address the board sends from."
351
+ "Two secrets, generated rather than chosen \u2014 32 characters or more each. Everything else the board reads from the database, cache and blob store this form links."
345
352
  ],
346
353
  ["envLink", `${templateRepositoryUrl}/blob/main/.env.example`],
347
354
  ["stores", JSON.stringify(VERCEL_MARKETPLACE_STORES)],
@@ -992,11 +999,14 @@ A forum, built on [Meith](${repositoryUrl}), running as Vercel functions.
992
999
  - **A Neon Postgres database**, attached to the project. Neon publishes the
993
1000
  pooled connection string as \`DATABASE_URL\` and the direct one as
994
1001
  \`DATABASE_URL_UNPOOLED\`.
995
- - **An Upstash Redis store**, attached the same way, for the shared cache.
996
- It publishes its own connection variable.
997
- - **A Vercel Blob store** for uploads, which publishes \`BLOB_READ_WRITE_TOKEN\`
998
- into the project by itself. This is the one value that used to be four hand-
999
- typed \`S3_*\` secrets.
1002
+ - **An Upstash Redis store**, attached the same way, for the shared cache. It
1003
+ publishes \`KV_URL\`, which the board reads as \`REDIS_URL\` \u2014 \`KV_REST_API_URL\`
1004
+ beside it is an HTTPS endpoint and is not used for this.
1005
+ - **A Vercel Blob store** for uploads, which publishes \`BLOB_STORE_ID\` into the
1006
+ project by itself. That is the whole credential: the board hands the id to
1007
+ Vercel's SDK, which authenticates with the deployment's own OIDC identity, so
1008
+ there is no token to copy. This is what used to be four hand-typed \`S3_*\`
1009
+ secrets.
1000
1010
  - **A Vercel project** carrying \`vercel.json\` \u2014 the build command
1001
1011
  \`${VERCEL_BUILD_COMMAND}\`,
1002
1012
  which applies the schema before it builds, materializes the board's app at
@@ -1004,8 +1014,11 @@ A forum, built on [Meith](${repositoryUrl}), running as Vercel functions.
1004
1014
  entry that drives the tick.
1005
1015
 
1006
1016
  **Mail is the one thing the button does not set up**, and it takes one click
1007
- after the deploy \u2014 see *Mail, in one click* below. The board boots and runs
1008
- without it, and delivers nothing, silently, until it is done.
1017
+ after the deploy \u2014 see *Mail, in one click* below. That is also where the
1018
+ address the board sends from goes: it has to be at a domain your provider has
1019
+ verified, which cannot be true of any domain before a provider exists. The
1020
+ board boots and runs without mail, and delivers nothing, silently, until it is
1021
+ done.
1009
1022
 
1010
1023
  ## What to type into the deploy form
1011
1024
 
@@ -1018,54 +1031,43 @@ openssl rand -hex 32 # CRON_SECRET
1018
1031
  \`\`\`
1019
1032
 
1020
1033
  \`CRON_SECRET\` is the name Vercel Cron sends, as \`Authorization: Bearer\`, and it
1021
- cannot be told to send another. Note that this floor is stricter than the 16
1034
+ cannot be told to send another \u2014 the caller is the platform, so this one has to
1035
+ be an environment variable both ends can read, and cannot be something the
1036
+ board makes up for itself. Note that this floor is stricter than the 16
1022
1037
  characters Vercel's own cron documentation suggests \u2014 a value generated by
1023
1038
  following those instructions is refused here, and the fix is a longer secret.
1024
1039
 
1025
- **Two driver values**, and they are the two that turn on an external service
1026
- the board then needs for every request:
1027
-
1028
- \`\`\`ini
1029
- ${VERCEL_TYPED_DRIVERS.join("\n")}
1030
- \`\`\`
1031
-
1032
- Neither is inferred, deliberately: the board caches in Redis and puts uploads
1033
- in an object store because you said so, never because a connection string or a
1034
- token happened to be present.
1040
+ \`AUTH_SECRET\` seals members' two-factor secrets and signs the unsubscribe links
1041
+ in outgoing mail. It stays in the environment deliberately: a copy of the
1042
+ database is then not enough to forge either.
1035
1043
 
1036
- Leaving them blank fails differently, which is why the form asks for both.
1037
- Blank \`FILESTORE_DRIVER\` means \`local\`, and the board **refuses to boot** on
1038
- this platform rather than write uploads to a disk that is about to disappear.
1039
- Blank \`CACHE_DRIVER\` complains about nothing: the board falls back to caching
1040
- inside each instance, and every instance then serves whatever it last saw for
1041
- up to a minute. That one degrades quietly, so type it.
1042
-
1043
- The remaining three drivers do derive themselves, and the form does not ask:
1044
- a \`DATABASE_URL\` means \`DATA_SOURCE=postgres\` and \`QUEUE_DRIVER=postgres\`, and
1045
- a \`RESEND_API_KEY\` with a \`MAIL_FROM\` beside it means \`MAIL_DRIVER=http\`:
1044
+ **That is the whole form.** Everything else the board works out from the stores
1045
+ this button just linked to the project:
1046
1046
 
1047
1047
  \`\`\`ini
1048
1048
  ${VERCEL_DERIVED_DRIVERS.join("\n")}
1049
1049
  \`\`\`
1050
1050
 
1051
- **Two connection strings copied from the stores the button just created.**
1052
- \`DIRECT_DATABASE_URL\` takes Neon's \`DATABASE_URL_UNPOOLED\` \u2014 migrations and the
1053
- first-run installer each hold a session-level advisory lock, which a
1054
- transaction-mode pooler cannot hold. \`REDIS_URL\` takes whatever variable the
1055
- Upstash store published.
1056
-
1057
- **The address the board sends from**: \`MAIL_FROM\`. This is the only mail value
1058
- you ever type. It has to be an address at a domain your mail provider has
1059
- verified for you, so no default is right and the board cannot guess it \u2014 see
1060
- *Mail, in one click* below. If you do not have one yet, put in the address you
1061
- intend to use and finish verifying it afterwards; nothing else waits on it.
1062
-
1063
- That is seven fields. If you would rather keep uploads somewhere you hold
1064
- yourself \u2014 see *Leaving Vercel* below for why that matters \u2014 set
1065
- \`FILESTORE_DRIVER=s3\` instead and add \`S3_BUCKET\`, \`S3_REGION\`,
1066
- \`S3_ACCESS_KEY_ID\` and \`S3_SECRET_ACCESS_KEY\` in the project's environment
1067
- settings, with \`S3_ENDPOINT\` for a bucket that is not AWS (\`S3_REGION=auto\` for
1068
- R2). The same board runs either way.
1051
+ \`DIRECT_DATABASE_URL\` comes from Neon's own \`DATABASE_URL_UNPOOLED\`, or
1052
+ \`POSTGRES_URL_NON_POOLING\` if that one is absent \u2014 migrations and the first-run
1053
+ installer each hold a session-level advisory lock, which the pooled
1054
+ \`DATABASE_URL\` cannot hold. \`REDIS_URL\` comes from Upstash's \`KV_URL\`, the one
1055
+ variable it publishes that speaks the Redis protocol.
1056
+
1057
+ Every one of those derivations is scoped to this platform, fires only where you
1058
+ have not set the variable yourself, and **refuses to boot rather than guess**.
1059
+ If a store is missing, or publishes a name this board does not know, the deploy
1060
+ stops with a message naming every variable it looked at \u2014 it will not fall back
1061
+ to caching inside each instance, or to uploads on a disk that is discarded with
1062
+ the instance. When the name is one we do not know, set \`REDIS_URL\` or
1063
+ \`DIRECT_DATABASE_URL\` in the project's environment settings and the derivation
1064
+ stands aside.
1065
+
1066
+ If you would rather keep uploads somewhere you hold yourself \u2014 see *Leaving
1067
+ Vercel* below for why that matters \u2014 set \`FILESTORE_DRIVER=s3\` and add
1068
+ \`S3_BUCKET\`, \`S3_REGION\`, \`S3_ACCESS_KEY_ID\` and \`S3_SECRET_ACCESS_KEY\` in the
1069
+ project's environment settings, with \`S3_ENDPOINT\` for a bucket that is not AWS
1070
+ (\`S3_REGION=auto\` for R2). The same board runs either way.
1069
1071
 
1070
1072
  ## Mail, in one click
1071
1073
 
@@ -1077,11 +1079,15 @@ invite anybody.
1077
1079
  the project and connects your sending domain.
1078
1080
  2. Verify that domain in the Resend dashboard if you have not already. Resend
1079
1081
  refuses to send from an address at a domain it has not verified.
1080
- 3. Redeploy, or let the next push redeploy.
1082
+ 3. Add \`MAIL_FROM\` to the project's environment settings \u2014 an address at that
1083
+ verified domain, and the only mail value you ever type. The deploy form does
1084
+ not ask for it, because a sender address is not something you can know
1085
+ before there is a provider to verify it.
1086
+ 4. Redeploy, or let the next push redeploy.
1081
1087
 
1082
1088
  That is all. The integration publishes its key into the project as
1083
1089
  \`RESEND_API_KEY\`, and the board reads that name: with it set, and \`MAIL_FROM\`
1084
- already in place, mail sends over Resend's HTTPS API with nothing further to
1090
+ beside it, mail sends over Resend's HTTPS API with nothing further to
1085
1091
  configure.
1086
1092
 
1087
1093
  The board is not tied to Resend. Its mail driver is a plain JSON-over-HTTPS
@@ -1178,13 +1184,19 @@ a Blob store does not:
1178
1184
  DATABASE_URL=\u2026 # Neon's pooled string
1179
1185
  DIRECT_DATABASE_URL=\u2026 # Neon's DATABASE_URL_UNPOOLED
1180
1186
  FILESTORE_DRIVER=blob
1181
- BLOB_READ_WRITE_TOKEN=\u2026 # copy it out of the project's environment settings
1187
+ BLOB_READ_WRITE_TOKEN=\u2026 # create one on the store; see below
1182
1188
  npm run community -- backup
1183
1189
  \`\`\`
1184
1190
 
1185
1191
  Run that from a checkout of this repository, with those four values in the
1186
1192
  environment \u2014 the CLI talks to Neon and to the Blob store over the network, so
1187
- it does not have to run on Vercel. The bundle it writes holds the dump *and*
1193
+ it does not have to run on Vercel.
1194
+
1195
+ That last one is the one value this route asks you to make by hand, and only
1196
+ here. On the deployment the board reaches the store with \`BLOB_STORE_ID\` and
1197
+ the deployment's OIDC identity, which a command on your own machine does not
1198
+ have. Open the store under **Storage**, create a read-write token, and use it
1199
+ for the backup; the board itself never needs it. The bundle it writes holds the dump *and*
1188
1200
  every object. Check the last line it prints: if it says *no uploads*, the
1189
1201
  uploads are not in the bundle and restoring it gives a board whose posts have
1190
1202
  broken images.
@@ -1317,7 +1329,7 @@ async function run(argv, version) {
1317
1329
  }
1318
1330
 
1319
1331
  // src/bin.ts
1320
- var result = await run(process.argv.slice(2), "0.18.0");
1332
+ var result = await run(process.argv.slice(2), "0.19.0");
1321
1333
  for (const line of result.lines) {
1322
1334
  if (result.code === 0) console.log(line);
1323
1335
  else console.error(line);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-meith",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Scaffold a Meith board — npx create-meith <name> writes a workspace that depends on @meith/web and @meith/cli.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/bin.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { run } from './cli'
3
3
 
4
- const result = await run(process.argv.slice(2), '0.18.0')
4
+ const result = await run(process.argv.slice(2), '0.19.0')
5
5
  for (const line of result.lines) {
6
6
  if (result.code === 0) console.log(line)
7
7
  else console.error(line)
package/src/scaffold.ts CHANGED
@@ -200,25 +200,24 @@ function vercelEnvExample(name: string): string {
200
200
  # no other instance can read and that is discarded with the instance — on Vercel
201
201
  # the board refuses it outright rather than losing uploads quietly.
202
202
  #
203
- # CACHE_DRIVER and FILESTORE_DRIVER are asked for at deploy time. The other
204
- # three derive themselves: a DATABASE_URL means postgres for the data source and
205
- # the queue, and a RESEND_API_KEY with a MAIL_FROM beside it means mail over the
206
- # provider's HTTPS API. Setting one here overrides the derivation, which is what
207
- # this file is for when you copy it to .env.local.
203
+ # The deploy form asks for none of them. On Vercel the board works each one out
204
+ # from what the linked stores publish: a DATABASE_URL means postgres for the
205
+ # data source and the queue, a Redis connection string means CACHE_DRIVER=redis,
206
+ # a Blob store's read-write token means FILESTORE_DRIVER=blob, and a
207
+ # RESEND_API_KEY with a MAIL_FROM beside it means mail over the provider's HTTPS
208
+ # API. Setting one here overrides the derivation, which is what this file is for
209
+ # when you copy it to .env.local.
208
210
  #
209
- # The two that stay explicit stay that way on purpose: each turns on an external
210
- # service the board then depends on for every request, and neither is switched on
211
- # by a variable that merely happens to be present. The board caches in Redis and
212
- # puts uploads in an object store because you said so.
211
+ # Every one of those derivations is scoped to Vercel, and each fires only from a
212
+ # value that is unambiguously the thing itself a redis:// or rediss:// URL, a
213
+ # read-write token. A board you run anywhere else is untouched by all of it and
214
+ # still takes these values from this file, exactly as it did before.
213
215
  #
214
- # What happens if you leave one blank differs, and the difference is the reason
215
- # the form asks for both. Leave FILESTORE_DRIVER blank and the board refuses to
216
- # boot at all: the default is local, and local on this platform means uploads
217
- # written to a disk that is about to disappear. Leave CACHE_DRIVER blank and
218
- # nothing complains the board falls back to caching inside each instance and
219
- # serves whatever that instance last saw, for up to a minute, however many
220
- # instances there are. That one degrades quietly, which is exactly why it is
221
- # worth typing rather than leaving to a default.
216
+ # A derivation that cannot resolve is a configuration error, not an invitation
217
+ # to pick something safe-looking. On Vercel, with the cache or the object store
218
+ # missing, the board refuses to boot and names every variable it looked at. It
219
+ # will not quietly cache inside the instance, and it will not quietly write
220
+ # uploads to a disk that is about to disappear.
222
221
  DATA_SOURCE=postgres
223
222
  QUEUE_DRIVER=postgres
224
223
  CACHE_DRIVER=redis
@@ -232,16 +231,20 @@ DATABASE_URL=
232
231
 
233
232
  ${ENV_DIRECT_DATABASE_URL_PROSE}
234
233
  #
235
- # On Vercel this is not optional. DATABASE_URL here is the pooler string, the
236
- # build runs \`community migrate\` against it, and /install takes the second of
237
- # those two session locks on first run. Neon publishes the direct string as
238
- # DATABASE_URL_UNPOOLED; copy that value into this variable.
234
+ # On Vercel this is not optional, and it is no longer yours to copy. DATABASE_URL
235
+ # here is the pooler string, the build runs \`community migrate\` against it, and
236
+ # /install takes the second of those two session locks on first run. Left blank,
237
+ # the board reads Neon's own direct string — \`DATABASE_URL_UNPOOLED\` first, then
238
+ # \`POSTGRES_URL_NON_POOLING\` — and refuses to boot if neither is there, naming
239
+ # both. Never \`POSTGRES_URL\`: that one is pooled.
239
240
  DIRECT_DATABASE_URL=
240
241
 
241
242
  # The shared cache — a Redis or Valkey endpoint, \`rediss://\` for TLS. Redis
242
243
  # holds cache entries and nothing else: losing it costs the board a warm cache,
243
- # not data, and signs nobody out. A store provisioned with the board publishes
244
- # its own connection variable; copy that value into this one.
244
+ # not data, and signs nobody out. Left blank on Vercel, the board reads the
245
+ # Upstash store's own \`KV_URL\`, which is the one variable it publishes that
246
+ # speaks the Redis protocol — \`KV_REST_API_URL\` is an HTTPS endpoint and is
247
+ # never used for this. A name we do not know goes here by hand.
245
248
  REDIS_URL=
246
249
 
247
250
  ${ENV_AUTH_SECRET_PROSE}
@@ -257,15 +260,26 @@ ${ENV_TICK_SECRET_PROSE}
257
260
  CRON_SECRET=
258
261
  # TICK_SECRET=
259
262
 
260
- # Uploads, in the Vercel Blob store the Deploy Button provisions. The store
261
- # publishes this variable into the project by itself, so there is nothing to
262
- # type and nothing to mistype but the board still needs FILESTORE_DRIVER=blob
263
- # above before it will use it. Every object is written with private access: an
264
- # object URL is not a public link, and member content is served by the board,
265
- # which is where permissions are checked.
263
+ # Uploads, in the Vercel Blob store the Deploy Button provisions. A store
264
+ # attached to the project publishes BLOB_STORE_ID and nothing else no token —
265
+ # because the SDK authenticates with the deployment's own OIDC identity: the
266
+ # board hands it the store id and lets it fetch the credential. There is nothing
267
+ # to type and nothing to mistype, and FILESTORE_DRIVER=blob derives from this
268
+ # variable being present.
266
269
  #
267
- # An upload is held whole in the instance's memory on the way in and on the way
268
- # out, so the function's memory limit, not the store, is what caps a file.
270
+ # BLOB_READ_WRITE_TOKEN is the other way in, and you make it yourself on the
271
+ # store. Set it when something has to reach the store from OUTSIDE a Vercel
272
+ # deployment — \`community backup\` run on your own machine is the case that
273
+ # matters — because there is no OIDC identity there to borrow. Set both and the
274
+ # board prefers the store id, unless the token names a different store, in which
275
+ # case the token wins: naming another store is a deliberate act.
276
+ #
277
+ # Every object is written with private access: an object URL is not a public
278
+ # link, and member content is served by the board, which is where permissions
279
+ # are checked. An upload is held whole in the instance's memory on the way in
280
+ # and on the way out, so the function's memory limit, not the store, is what
281
+ # caps a file.
282
+ BLOB_STORE_ID=
269
283
  BLOB_READ_WRITE_TOKEN=
270
284
 
271
285
  # Uploads in an S3-compatible bucket instead — AWS, R2, MinIO, Spaces. This is
@@ -288,10 +302,10 @@ BLOB_READ_WRITE_TOKEN=
288
302
  # board refuses it on Vercel; 587 with STARTTLS may work, but an API does not
289
303
  # depend on the platform's egress rules staying as they are.
290
304
  #
291
- # MAIL_FROM is yours to decide and the one mail value the button asks for: it
292
- # must be an address at a domain the provider has verified for you, and no
293
- # default is right. Send from an unverified sender and the provider rejects
294
- # every message.
305
+ # MAIL_FROM is yours to decide, and it belongs to the step that adds Resend
306
+ # rather than to the deploy: it must be an address at a domain the provider has
307
+ # verified for you, no default is right, and until a provider is added the board
308
+ # cannot send from any address at all. Set it alongside the integration.
295
309
  MAIL_FROM=
296
310
 
297
311
  # Add the Resend integration to the project from Vercel's marketplace and it
@@ -335,23 +349,15 @@ const SELF_HOST_DEPLOY_KIT = [
335
349
  'docker-healthcheck.sh',
336
350
  ] as const
337
351
 
338
- const VERCEL_TYPED_DRIVERS = ['CACHE_DRIVER=redis', 'FILESTORE_DRIVER=blob'] as const
339
-
340
352
  const VERCEL_DERIVED_DRIVERS = [
341
353
  'DATA_SOURCE=postgres',
342
354
  'QUEUE_DRIVER=postgres',
355
+ 'CACHE_DRIVER=redis',
356
+ 'FILESTORE_DRIVER=blob',
343
357
  'MAIL_DRIVER=http',
344
358
  ] as const
345
359
 
346
- export const VERCEL_PROMPTED_ENV = [
347
- 'CACHE_DRIVER',
348
- 'FILESTORE_DRIVER',
349
- 'DIRECT_DATABASE_URL',
350
- 'REDIS_URL',
351
- 'AUTH_SECRET',
352
- 'CRON_SECRET',
353
- 'MAIL_FROM',
354
- ] as const
360
+ export const VERCEL_PROMPTED_ENV = ['AUTH_SECRET', 'CRON_SECRET'] as const
355
361
 
356
362
  export const VERCEL_MARKETPLACE_STORES = [
357
363
  { type: 'integration', integrationSlug: 'neon', productSlug: 'neon', protocol: 'storage' },
@@ -372,7 +378,7 @@ export function deployButtonUrl(templateRepositoryUrl: string): string {
372
378
  ['env', VERCEL_PROMPTED_ENV.join(',')],
373
379
  [
374
380
  'envDescription',
375
- 'Two driver values, two secrets to generate, the direct database URL, the cache URL, and the verified address the board sends from.',
381
+ 'Two secrets, generated rather than chosen 32 characters or more each. Everything else the board reads from the database, cache and blob store this form links.',
376
382
  ],
377
383
  ['envLink', `${templateRepositoryUrl}/blob/main/.env.example`],
378
384
  ['stores', JSON.stringify(VERCEL_MARKETPLACE_STORES)],
@@ -1054,11 +1060,14 @@ A forum, built on [Meith](${repositoryUrl}), running as Vercel functions.
1054
1060
  - **A Neon Postgres database**, attached to the project. Neon publishes the
1055
1061
  pooled connection string as \`DATABASE_URL\` and the direct one as
1056
1062
  \`DATABASE_URL_UNPOOLED\`.
1057
- - **An Upstash Redis store**, attached the same way, for the shared cache.
1058
- It publishes its own connection variable.
1059
- - **A Vercel Blob store** for uploads, which publishes \`BLOB_READ_WRITE_TOKEN\`
1060
- into the project by itself. This is the one value that used to be four hand-
1061
- typed \`S3_*\` secrets.
1063
+ - **An Upstash Redis store**, attached the same way, for the shared cache. It
1064
+ publishes \`KV_URL\`, which the board reads as \`REDIS_URL\` — \`KV_REST_API_URL\`
1065
+ beside it is an HTTPS endpoint and is not used for this.
1066
+ - **A Vercel Blob store** for uploads, which publishes \`BLOB_STORE_ID\` into the
1067
+ project by itself. That is the whole credential: the board hands the id to
1068
+ Vercel's SDK, which authenticates with the deployment's own OIDC identity, so
1069
+ there is no token to copy. This is what used to be four hand-typed \`S3_*\`
1070
+ secrets.
1062
1071
  - **A Vercel project** carrying \`vercel.json\` — the build command
1063
1072
  \`${VERCEL_BUILD_COMMAND}\`,
1064
1073
  which applies the schema before it builds, materializes the board's app at
@@ -1066,8 +1075,11 @@ A forum, built on [Meith](${repositoryUrl}), running as Vercel functions.
1066
1075
  entry that drives the tick.
1067
1076
 
1068
1077
  **Mail is the one thing the button does not set up**, and it takes one click
1069
- after the deploy — see *Mail, in one click* below. The board boots and runs
1070
- without it, and delivers nothing, silently, until it is done.
1078
+ after the deploy — see *Mail, in one click* below. That is also where the
1079
+ address the board sends from goes: it has to be at a domain your provider has
1080
+ verified, which cannot be true of any domain before a provider exists. The
1081
+ board boots and runs without mail, and delivers nothing, silently, until it is
1082
+ done.
1071
1083
 
1072
1084
  ## What to type into the deploy form
1073
1085
 
@@ -1080,54 +1092,43 @@ openssl rand -hex 32 # CRON_SECRET
1080
1092
  \`\`\`
1081
1093
 
1082
1094
  \`CRON_SECRET\` is the name Vercel Cron sends, as \`Authorization: Bearer\`, and it
1083
- cannot be told to send another. Note that this floor is stricter than the 16
1095
+ cannot be told to send another the caller is the platform, so this one has to
1096
+ be an environment variable both ends can read, and cannot be something the
1097
+ board makes up for itself. Note that this floor is stricter than the 16
1084
1098
  characters Vercel's own cron documentation suggests — a value generated by
1085
1099
  following those instructions is refused here, and the fix is a longer secret.
1086
1100
 
1087
- **Two driver values**, and they are the two that turn on an external service
1088
- the board then needs for every request:
1101
+ \`AUTH_SECRET\` seals members' two-factor secrets and signs the unsubscribe links
1102
+ in outgoing mail. It stays in the environment deliberately: a copy of the
1103
+ database is then not enough to forge either.
1089
1104
 
1090
- \`\`\`ini
1091
- ${VERCEL_TYPED_DRIVERS.join('\n')}
1092
- \`\`\`
1093
-
1094
- Neither is inferred, deliberately: the board caches in Redis and puts uploads
1095
- in an object store because you said so, never because a connection string or a
1096
- token happened to be present.
1097
-
1098
- Leaving them blank fails differently, which is why the form asks for both.
1099
- Blank \`FILESTORE_DRIVER\` means \`local\`, and the board **refuses to boot** on
1100
- this platform rather than write uploads to a disk that is about to disappear.
1101
- Blank \`CACHE_DRIVER\` complains about nothing: the board falls back to caching
1102
- inside each instance, and every instance then serves whatever it last saw for
1103
- up to a minute. That one degrades quietly, so type it.
1104
-
1105
- The remaining three drivers do derive themselves, and the form does not ask:
1106
- a \`DATABASE_URL\` means \`DATA_SOURCE=postgres\` and \`QUEUE_DRIVER=postgres\`, and
1107
- a \`RESEND_API_KEY\` with a \`MAIL_FROM\` beside it means \`MAIL_DRIVER=http\`:
1105
+ **That is the whole form.** Everything else the board works out from the stores
1106
+ this button just linked to the project:
1108
1107
 
1109
1108
  \`\`\`ini
1110
1109
  ${VERCEL_DERIVED_DRIVERS.join('\n')}
1111
1110
  \`\`\`
1112
1111
 
1113
- **Two connection strings copied from the stores the button just created.**
1114
- \`DIRECT_DATABASE_URL\` takes Neon's \`DATABASE_URL_UNPOOLED\` — migrations and the
1115
- first-run installer each hold a session-level advisory lock, which a
1116
- transaction-mode pooler cannot hold. \`REDIS_URL\` takes whatever variable the
1117
- Upstash store published.
1118
-
1119
- **The address the board sends from**: \`MAIL_FROM\`. This is the only mail value
1120
- you ever type. It has to be an address at a domain your mail provider has
1121
- verified for you, so no default is right and the board cannot guess it see
1122
- *Mail, in one click* below. If you do not have one yet, put in the address you
1123
- intend to use and finish verifying it afterwards; nothing else waits on it.
1124
-
1125
- That is seven fields. If you would rather keep uploads somewhere you hold
1126
- yourself — see *Leaving Vercel* below for why that matters — set
1127
- \`FILESTORE_DRIVER=s3\` instead and add \`S3_BUCKET\`, \`S3_REGION\`,
1128
- \`S3_ACCESS_KEY_ID\` and \`S3_SECRET_ACCESS_KEY\` in the project's environment
1129
- settings, with \`S3_ENDPOINT\` for a bucket that is not AWS (\`S3_REGION=auto\` for
1130
- R2). The same board runs either way.
1112
+ \`DIRECT_DATABASE_URL\` comes from Neon's own \`DATABASE_URL_UNPOOLED\`, or
1113
+ \`POSTGRES_URL_NON_POOLING\` if that one is absent — migrations and the first-run
1114
+ installer each hold a session-level advisory lock, which the pooled
1115
+ \`DATABASE_URL\` cannot hold. \`REDIS_URL\` comes from Upstash's \`KV_URL\`, the one
1116
+ variable it publishes that speaks the Redis protocol.
1117
+
1118
+ Every one of those derivations is scoped to this platform, fires only where you
1119
+ have not set the variable yourself, and **refuses to boot rather than guess**.
1120
+ If a store is missing, or publishes a name this board does not know, the deploy
1121
+ stops with a message naming every variable it looked at it will not fall back
1122
+ to caching inside each instance, or to uploads on a disk that is discarded with
1123
+ the instance. When the name is one we do not know, set \`REDIS_URL\` or
1124
+ \`DIRECT_DATABASE_URL\` in the project's environment settings and the derivation
1125
+ stands aside.
1126
+
1127
+ If you would rather keep uploads somewhere you hold yourself — see *Leaving
1128
+ Vercel* below for why that matters set \`FILESTORE_DRIVER=s3\` and add
1129
+ \`S3_BUCKET\`, \`S3_REGION\`, \`S3_ACCESS_KEY_ID\` and \`S3_SECRET_ACCESS_KEY\` in the
1130
+ project's environment settings, with \`S3_ENDPOINT\` for a bucket that is not AWS
1131
+ (\`S3_REGION=auto\` for R2). The same board runs either way.
1131
1132
 
1132
1133
  ## Mail, in one click
1133
1134
 
@@ -1139,11 +1140,15 @@ invite anybody.
1139
1140
  the project and connects your sending domain.
1140
1141
  2. Verify that domain in the Resend dashboard if you have not already. Resend
1141
1142
  refuses to send from an address at a domain it has not verified.
1142
- 3. Redeploy, or let the next push redeploy.
1143
+ 3. Add \`MAIL_FROM\` to the project's environment settings — an address at that
1144
+ verified domain, and the only mail value you ever type. The deploy form does
1145
+ not ask for it, because a sender address is not something you can know
1146
+ before there is a provider to verify it.
1147
+ 4. Redeploy, or let the next push redeploy.
1143
1148
 
1144
1149
  That is all. The integration publishes its key into the project as
1145
1150
  \`RESEND_API_KEY\`, and the board reads that name: with it set, and \`MAIL_FROM\`
1146
- already in place, mail sends over Resend's HTTPS API with nothing further to
1151
+ beside it, mail sends over Resend's HTTPS API with nothing further to
1147
1152
  configure.
1148
1153
 
1149
1154
  The board is not tied to Resend. Its mail driver is a plain JSON-over-HTTPS
@@ -1240,13 +1245,19 @@ a Blob store does not:
1240
1245
  DATABASE_URL=… # Neon's pooled string
1241
1246
  DIRECT_DATABASE_URL=… # Neon's DATABASE_URL_UNPOOLED
1242
1247
  FILESTORE_DRIVER=blob
1243
- BLOB_READ_WRITE_TOKEN=… # copy it out of the project's environment settings
1248
+ BLOB_READ_WRITE_TOKEN=… # create one on the store; see below
1244
1249
  npm run community -- backup
1245
1250
  \`\`\`
1246
1251
 
1247
1252
  Run that from a checkout of this repository, with those four values in the
1248
1253
  environment — the CLI talks to Neon and to the Blob store over the network, so
1249
- it does not have to run on Vercel. The bundle it writes holds the dump *and*
1254
+ it does not have to run on Vercel.
1255
+
1256
+ That last one is the one value this route asks you to make by hand, and only
1257
+ here. On the deployment the board reaches the store with \`BLOB_STORE_ID\` and
1258
+ the deployment's OIDC identity, which a command on your own machine does not
1259
+ have. Open the store under **Storage**, create a read-write token, and use it
1260
+ for the backup; the board itself never needs it. The bundle it writes holds the dump *and*
1250
1261
  every object. Check the last line it prints: if it says *no uploads*, the
1251
1262
  uploads are not in the bundle and restoring it gives a board whose posts have
1252
1263
  broken images.