create-meith 0.19.0 → 0.21.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
@@ -25,7 +25,8 @@ var MATERIALIZED_AT_ROOT = [
25
25
  ];
26
26
  var VERCEL_BUILD_COMMAND = `community migrate && forum-web build ${AT_ROOT_FLAG}`;
27
27
  var TICK_PATH = "/api/system/tick";
28
- var TICK_SCHEDULE = "* * * * *";
28
+ var TICK_SCHEDULE = "0 3 * * *";
29
+ var RESEND_SENDER_MAILBOX = "noreply";
29
30
  var MATERIALIZED_PUBLIC = [
30
31
  "placeholder-logo.png",
31
32
  "placeholder-logo.svg",
@@ -278,15 +279,17 @@ BLOB_READ_WRITE_TOKEN=
278
279
  # board refuses it on Vercel; 587 with STARTTLS may work, but an API does not
279
280
  # depend on the platform's egress rules staying as they are.
280
281
  #
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.
282
+ # MAIL_FROM is optional where a provider publishes the domain it sends from:
283
+ # with RESEND_API_KEY and RESEND_EMAIL_DOMAIN both set, the board sends from
284
+ # noreply@ that domain and this can stay empty. Set it to send from another
285
+ # address \u2014 it must be at a domain the provider has verified for you, and an
286
+ # address set here always wins over the derived one.
285
287
  MAIL_FROM=
286
288
 
287
289
  # Add the Resend integration to the project from Vercel's marketplace and it
288
- # publishes its key under this name, which the board reads: with RESEND_API_KEY
289
- # set, and MAIL_FROM beside it, the board sends over Resend's HTTPS API and
290
+ # publishes its key and its sending domain under these names, which the board
291
+ # reads: with RESEND_API_KEY set, and either RESEND_EMAIL_DOMAIN or a MAIL_FROM
292
+ # beside it, the board sends over Resend's HTTPS API and
290
293
  # needs neither of the two variables below. The mail driver itself is a plain
291
294
  # JSON-over-HTTPS sender and is not Resend-specific \u2014 this is one injected name
292
295
  # bridged to the generic pair, not a provider baked into the board.
@@ -330,7 +333,7 @@ var VERCEL_DERIVED_DRIVERS = [
330
333
  "MAIL_DRIVER=http"
331
334
  ];
332
335
  var VERCEL_PROMPTED_ENV = ["AUTH_SECRET", "CRON_SECRET"];
333
- var VERCEL_MARKETPLACE_STORES = [
336
+ var VERCEL_MARKETPLACE_PRODUCTS = [
334
337
  { type: "integration", integrationSlug: "neon", productSlug: "neon", protocol: "storage" },
335
338
  {
336
339
  type: "integration",
@@ -338,7 +341,13 @@ var VERCEL_MARKETPLACE_STORES = [
338
341
  productSlug: "upstash-kv",
339
342
  protocol: "storage"
340
343
  },
341
- { type: "blob" }
344
+ { type: "blob" },
345
+ {
346
+ type: "integration",
347
+ integrationSlug: "resend",
348
+ productSlug: "resend-email",
349
+ protocol: "messaging"
350
+ }
342
351
  ];
343
352
  function deployButtonUrl(templateRepositoryUrl) {
344
353
  const params = new URLSearchParams([
@@ -348,10 +357,10 @@ function deployButtonUrl(templateRepositoryUrl) {
348
357
  ["env", VERCEL_PROMPTED_ENV.join(",")],
349
358
  [
350
359
  "envDescription",
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."
360
+ "Two secrets, generated rather than chosen \u2014 32 characters or more each. Everything else the board reads from the database, cache, blob store and mail provider this form links."
352
361
  ],
353
362
  ["envLink", `${templateRepositoryUrl}/blob/main/.env.example`],
354
- ["stores", JSON.stringify(VERCEL_MARKETPLACE_STORES)],
363
+ ["products", JSON.stringify(VERCEL_MARKETPLACE_PRODUCTS)],
355
364
  ["skippable-integrations", "1"]
356
365
  ]);
357
366
  return `https://vercel.com/new/clone?${params.toString()}`;
@@ -429,6 +438,7 @@ import { defineForumConfig } from '@meith/web/config'
429
438
  import {
430
439
  BROWSER_THEME_COLOR,
431
440
  DARK_TOKENS,
441
+ defaultMessages,
432
442
  defaultTheme,
433
443
  LIGHT_TOKENS,
434
444
  } from '@meith/theme-default'
@@ -443,6 +453,7 @@ export default defineForumConfig({
443
453
  tokens: { light: LIGHT_TOKENS, dark: DARK_TOKENS },
444
454
  browserThemeColor: BROWSER_THEME_COLOR,
445
455
  theme: defaultTheme,
456
+ messages: defaultMessages,
446
457
  },
447
458
  },
448
459
  defaultTheme: 'default',
@@ -1007,18 +1018,20 @@ A forum, built on [Meith](${repositoryUrl}), running as Vercel functions.
1007
1018
  Vercel's SDK, which authenticates with the deployment's own OIDC identity, so
1008
1019
  there is no token to copy. This is what used to be four hand-typed \`S3_*\`
1009
1020
  secrets.
1021
+ - **A Resend mail account**, attached the same way, which publishes
1022
+ \`RESEND_API_KEY\` and \`RESEND_EMAIL_DOMAIN\`. The board reads both names
1023
+ directly: its mail driver already speaks Resend's request shape, so there is
1024
+ nothing to adapt, and the sending domain is what the sender is built from.
1010
1025
  - **A Vercel project** carrying \`vercel.json\` \u2014 the build command
1011
1026
  \`${VERCEL_BUILD_COMMAND}\`,
1012
1027
  which applies the schema before it builds, materializes the board's app at
1013
1028
  the project root so the artefact lands where Vercel reads it, and the cron
1014
1029
  entry that drives the tick.
1015
1030
 
1016
- **Mail is the one thing the button does not set up**, and it takes one click
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.
1031
+ **Mail needs no variables after the deploy.** Resend publishes both its key
1032
+ and its sending domain, and the board sends from \`${RESEND_SENDER_MAILBOX}@\`
1033
+ that domain \u2014 see *Mail* below to send from a different address, and for the
1034
+ one case that does need you: a domain Resend has not verified yet.
1022
1035
 
1023
1036
  ## What to type into the deploy form
1024
1037
 
@@ -1069,26 +1082,30 @@ Vercel* below for why that matters \u2014 set \`FILESTORE_DRIVER=s3\` and add
1069
1082
  project's environment settings, with \`S3_ENDPOINT\` for a bucket that is not AWS
1070
1083
  (\`S3_REGION=auto\` for R2). The same board runs either way.
1071
1084
 
1072
- ## Mail, in one click
1085
+ ## Mail
1086
+
1087
+ **There is nothing to set.** The Resend the deploy form added publishes two
1088
+ names into the project: \`RESEND_API_KEY\`, and \`RESEND_EMAIL_DOMAIN\` \u2014 the
1089
+ domain it sends from. The board reads both, sends from
1090
+ \`${RESEND_SENDER_MAILBOX}@\` that domain, and posts over Resend's HTTPS API.
1073
1091
 
1074
- A board that cannot send mail cannot reset a password, so do this before you
1075
- invite anybody.
1092
+ **If Resend refuses the messages**, that domain is not verified yet. Resend
1093
+ will not send from a domain it has not verified, whoever set the address, so
1094
+ verify it from the Resend dashboard \u2014 the deploy cannot do that step for you,
1095
+ because it is Resend confirming you own the domain. This is the one thing here
1096
+ that can need attention, and it announces itself: the test button below says
1097
+ so rather than the board failing quietly.
1076
1098
 
1077
- 1. Open your project on Vercel, go to **Storage \u2192 Marketplace** (or
1078
- **Integrations**), and add **Resend**. It creates a Resend account linked to
1079
- the project and connects your sending domain.
1080
- 2. Verify that domain in the Resend dashboard if you have not already. Resend
1081
- refuses to send from an address at a domain it has not verified.
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.
1099
+ **To send from a different address**, set \`MAIL_FROM\` in the project's
1100
+ environment settings and redeploy. It must be at a domain Resend has verified,
1101
+ for the same reason. An address you set always wins over the derived one.
1087
1102
 
1088
- That is all. The integration publishes its key into the project as
1089
- \`RESEND_API_KEY\`, and the board reads that name: with it set, and \`MAIL_FROM\`
1090
- beside it, mail sends over Resend's HTTPS API with nothing further to
1091
- configure.
1103
+ A board with the key but no verified domain \u2014 which is what you get if you
1104
+ remove the integration's \`RESEND_EMAIL_DOMAIN\` without putting a
1105
+ \`MAIL_FROM\` in its place \u2014 does not guess a sender. It stays on the log
1106
+ driver and delivers nothing, which is the honest outcome: a guessed sender at
1107
+ an unverified domain would be refused by Resend anyway, one message at a
1108
+ time.
1092
1109
 
1093
1110
  The board is not tied to Resend. Its mail driver is a plain JSON-over-HTTPS
1094
1111
  sender that posts \`{from, to, subject, text, html, reply_to}\` with a bearer
@@ -1097,8 +1114,10 @@ it needs no adapter. Any provider with the same shape works: set
1097
1114
  \`MAIL_HTTP_ENDPOINT\`, \`MAIL_HTTP_TOKEN\` and \`MAIL_DRIVER=http\` in the
1098
1115
  project's environment settings, and set the first two **together** \u2014 either
1099
1116
  one on its own stands the Resend bridge down, so a key issued for Resend is
1100
- never presented to an endpoint you chose. Only \`RESEND_API_KEY\` turns the
1101
- driver on by itself. Delete it once you have moved off Resend.
1117
+ never presented to an endpoint you chose. Setting \`MAIL_DRIVER\` to anything
1118
+ but \`http\` stands the bridge down too, for the same reason: a board that
1119
+ moved to SMTP must not send through its new provider from Resend's domain.
1120
+ Delete \`RESEND_API_KEY\` once you have moved off Resend.
1102
1121
 
1103
1122
  Check it worked: sign in as the administrator and use the test button on
1104
1123
  **/admin \u2192 Settings \u2192 Mail**.
@@ -1122,15 +1141,23 @@ drains; nothing here runs it on its own, because there is no worker process on
1122
1141
  a function platform. Two things about it are worth knowing **before** you
1123
1142
  deploy rather than after:
1124
1143
 
1125
- - **A per-minute schedule needs a paid plan.** Hobby allows a couple of cron
1126
- jobs and runs each of them roughly once a day, at an hour Vercel chooses;
1127
- only paid plans accept an arbitrary cron expression. A board ticking daily
1128
- still loses nothing \u2014 tasks are written so a missed run delays work rather
1129
- than dropping it \u2014 but "as it happens" notifications become a daily digest in
1130
- all but name. To keep a minute-by-minute tick on Hobby, drive
1131
- \`${TICK_PATH}\` from something else that can call a URL on a schedule \u2014 a
1132
- GitHub Actions workflow, a systemd timer, an uptime pinger \u2014 presenting
1133
- \`TICK_SECRET\` instead.
1144
+ - **This ships a daily schedule, because Hobby refuses anything faster.** A
1145
+ Hobby plan rejects a cron expression that would run more than once a day \u2014
1146
+ the deployment fails outright rather than being slowed down \u2014 so
1147
+ \`vercel.json\` carries \`${TICK_SCHEDULE}\` and deploys anywhere. On a
1148
+ paid plan, edit it to \`* * * * *\` and the board ticks every minute.
1149
+
1150
+ A daily tick loses nothing permanently: tasks are written so a missed run
1151
+ delays work rather than dropping it, and a password reset is sent as the
1152
+ request is handled rather than waiting for a tick. What it does delay is
1153
+ everything the tick drives \u2014 a new post is not findable in search, and a
1154
+ notification is not sent, until the next run.
1155
+
1156
+ **To keep a fast tick without paying**, drive \`${TICK_PATH}\` from anything
1157
+ that can call a URL on a schedule \u2014 a GitHub Actions workflow, a systemd
1158
+ timer, an uptime pinger \u2014 presenting \`TICK_SECRET\` instead of
1159
+ \`CRON_SECRET\`. The endpoint accepts either, so the Vercel cron and an
1160
+ outside scheduler can both drive it.
1134
1161
  - **\`maxDuration = 300\` is validated when the project builds, not when the
1135
1162
  function runs.** A plan that does not allow 300 seconds therefore **fails the
1136
1163
  deployment** rather than clamping the request. With Fluid Compute \u2014 the
@@ -1329,7 +1356,7 @@ async function run(argv, version) {
1329
1356
  }
1330
1357
 
1331
1358
  // src/bin.ts
1332
- var result = await run(process.argv.slice(2), "0.19.0");
1359
+ var result = await run(process.argv.slice(2), "0.21.0");
1333
1360
  for (const line of result.lines) {
1334
1361
  if (result.code === 0) console.log(line);
1335
1362
  else console.error(line);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-meith",
3
- "version": "0.19.0",
3
+ "version": "0.21.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.19.0')
4
+ const result = await run(process.argv.slice(2), '0.21.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/index.ts CHANGED
@@ -10,7 +10,7 @@ export {
10
10
  TICK_PATH,
11
11
  TICK_SCHEDULE,
12
12
  VERCEL_BUILD_COMMAND,
13
- VERCEL_MARKETPLACE_STORES,
13
+ VERCEL_MARKETPLACE_PRODUCTS,
14
14
  VERCEL_PROMPTED_ENV,
15
15
  validateName,
16
16
  } from './scaffold'
package/src/scaffold.ts CHANGED
@@ -33,7 +33,9 @@ export const VERCEL_BUILD_COMMAND = `community migrate && forum-web build ${AT_R
33
33
 
34
34
  export const TICK_PATH = '/api/system/tick'
35
35
 
36
- export const TICK_SCHEDULE = '* * * * *'
36
+ export const TICK_SCHEDULE = '0 3 * * *'
37
+
38
+ export const RESEND_SENDER_MAILBOX = 'noreply'
37
39
 
38
40
  export const MATERIALIZED_PUBLIC = [
39
41
  'placeholder-logo.png',
@@ -302,15 +304,17 @@ BLOB_READ_WRITE_TOKEN=
302
304
  # board refuses it on Vercel; 587 with STARTTLS may work, but an API does not
303
305
  # depend on the platform's egress rules staying as they are.
304
306
  #
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.
307
+ # MAIL_FROM is optional where a provider publishes the domain it sends from:
308
+ # with RESEND_API_KEY and RESEND_EMAIL_DOMAIN both set, the board sends from
309
+ # noreply@ that domain and this can stay empty. Set it to send from another
310
+ # address it must be at a domain the provider has verified for you, and an
311
+ # address set here always wins over the derived one.
309
312
  MAIL_FROM=
310
313
 
311
314
  # Add the Resend integration to the project from Vercel's marketplace and it
312
- # publishes its key under this name, which the board reads: with RESEND_API_KEY
313
- # set, and MAIL_FROM beside it, the board sends over Resend's HTTPS API and
315
+ # publishes its key and its sending domain under these names, which the board
316
+ # reads: with RESEND_API_KEY set, and either RESEND_EMAIL_DOMAIN or a MAIL_FROM
317
+ # beside it, the board sends over Resend's HTTPS API and
314
318
  # needs neither of the two variables below. The mail driver itself is a plain
315
319
  # JSON-over-HTTPS sender and is not Resend-specific — this is one injected name
316
320
  # bridged to the generic pair, not a provider baked into the board.
@@ -359,7 +363,7 @@ const VERCEL_DERIVED_DRIVERS = [
359
363
 
360
364
  export const VERCEL_PROMPTED_ENV = ['AUTH_SECRET', 'CRON_SECRET'] as const
361
365
 
362
- export const VERCEL_MARKETPLACE_STORES = [
366
+ export const VERCEL_MARKETPLACE_PRODUCTS = [
363
367
  { type: 'integration', integrationSlug: 'neon', productSlug: 'neon', protocol: 'storage' },
364
368
  {
365
369
  type: 'integration',
@@ -368,6 +372,12 @@ export const VERCEL_MARKETPLACE_STORES = [
368
372
  protocol: 'storage',
369
373
  },
370
374
  { type: 'blob' },
375
+ {
376
+ type: 'integration',
377
+ integrationSlug: 'resend',
378
+ productSlug: 'resend-email',
379
+ protocol: 'messaging',
380
+ },
371
381
  ] as const
372
382
 
373
383
  export function deployButtonUrl(templateRepositoryUrl: string): string {
@@ -378,10 +388,10 @@ export function deployButtonUrl(templateRepositoryUrl: string): string {
378
388
  ['env', VERCEL_PROMPTED_ENV.join(',')],
379
389
  [
380
390
  'envDescription',
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.',
391
+ 'Two secrets, generated rather than chosen — 32 characters or more each. Everything else the board reads from the database, cache, blob store and mail provider this form links.',
382
392
  ],
383
393
  ['envLink', `${templateRepositoryUrl}/blob/main/.env.example`],
384
- ['stores', JSON.stringify(VERCEL_MARKETPLACE_STORES)],
394
+ ['products', JSON.stringify(VERCEL_MARKETPLACE_PRODUCTS)],
385
395
  ['skippable-integrations', '1'],
386
396
  ])
387
397
 
@@ -463,6 +473,7 @@ import { defineForumConfig } from '@meith/web/config'
463
473
  import {
464
474
  BROWSER_THEME_COLOR,
465
475
  DARK_TOKENS,
476
+ defaultMessages,
466
477
  defaultTheme,
467
478
  LIGHT_TOKENS,
468
479
  } from '@meith/theme-default'
@@ -477,6 +488,7 @@ export default defineForumConfig({
477
488
  tokens: { light: LIGHT_TOKENS, dark: DARK_TOKENS },
478
489
  browserThemeColor: BROWSER_THEME_COLOR,
479
490
  theme: defaultTheme,
491
+ messages: defaultMessages,
480
492
  },
481
493
  },
482
494
  defaultTheme: 'default',
@@ -1068,18 +1080,20 @@ A forum, built on [Meith](${repositoryUrl}), running as Vercel functions.
1068
1080
  Vercel's SDK, which authenticates with the deployment's own OIDC identity, so
1069
1081
  there is no token to copy. This is what used to be four hand-typed \`S3_*\`
1070
1082
  secrets.
1083
+ - **A Resend mail account**, attached the same way, which publishes
1084
+ \`RESEND_API_KEY\` and \`RESEND_EMAIL_DOMAIN\`. The board reads both names
1085
+ directly: its mail driver already speaks Resend's request shape, so there is
1086
+ nothing to adapt, and the sending domain is what the sender is built from.
1071
1087
  - **A Vercel project** carrying \`vercel.json\` — the build command
1072
1088
  \`${VERCEL_BUILD_COMMAND}\`,
1073
1089
  which applies the schema before it builds, materializes the board's app at
1074
1090
  the project root so the artefact lands where Vercel reads it, and the cron
1075
1091
  entry that drives the tick.
1076
1092
 
1077
- **Mail is the one thing the button does not set up**, and it takes one click
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.
1093
+ **Mail needs no variables after the deploy.** Resend publishes both its key
1094
+ and its sending domain, and the board sends from \`${RESEND_SENDER_MAILBOX}@\`
1095
+ that domain see *Mail* below to send from a different address, and for the
1096
+ one case that does need you: a domain Resend has not verified yet.
1083
1097
 
1084
1098
  ## What to type into the deploy form
1085
1099
 
@@ -1130,26 +1144,30 @@ Vercel* below for why that matters — set \`FILESTORE_DRIVER=s3\` and add
1130
1144
  project's environment settings, with \`S3_ENDPOINT\` for a bucket that is not AWS
1131
1145
  (\`S3_REGION=auto\` for R2). The same board runs either way.
1132
1146
 
1133
- ## Mail, in one click
1147
+ ## Mail
1148
+
1149
+ **There is nothing to set.** The Resend the deploy form added publishes two
1150
+ names into the project: \`RESEND_API_KEY\`, and \`RESEND_EMAIL_DOMAIN\` — the
1151
+ domain it sends from. The board reads both, sends from
1152
+ \`${RESEND_SENDER_MAILBOX}@\` that domain, and posts over Resend's HTTPS API.
1134
1153
 
1135
- A board that cannot send mail cannot reset a password, so do this before you
1136
- invite anybody.
1154
+ **If Resend refuses the messages**, that domain is not verified yet. Resend
1155
+ will not send from a domain it has not verified, whoever set the address, so
1156
+ verify it from the Resend dashboard — the deploy cannot do that step for you,
1157
+ because it is Resend confirming you own the domain. This is the one thing here
1158
+ that can need attention, and it announces itself: the test button below says
1159
+ so rather than the board failing quietly.
1137
1160
 
1138
- 1. Open your project on Vercel, go to **Storage Marketplace** (or
1139
- **Integrations**), and add **Resend**. It creates a Resend account linked to
1140
- the project and connects your sending domain.
1141
- 2. Verify that domain in the Resend dashboard if you have not already. Resend
1142
- refuses to send from an address at a domain it has not verified.
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.
1161
+ **To send from a different address**, set \`MAIL_FROM\` in the project's
1162
+ environment settings and redeploy. It must be at a domain Resend has verified,
1163
+ for the same reason. An address you set always wins over the derived one.
1148
1164
 
1149
- That is all. The integration publishes its key into the project as
1150
- \`RESEND_API_KEY\`, and the board reads that name: with it set, and \`MAIL_FROM\`
1151
- beside it, mail sends over Resend's HTTPS API with nothing further to
1152
- configure.
1165
+ A board with the key but no verified domain which is what you get if you
1166
+ remove the integration's \`RESEND_EMAIL_DOMAIN\` without putting a
1167
+ \`MAIL_FROM\` in its place does not guess a sender. It stays on the log
1168
+ driver and delivers nothing, which is the honest outcome: a guessed sender at
1169
+ an unverified domain would be refused by Resend anyway, one message at a
1170
+ time.
1153
1171
 
1154
1172
  The board is not tied to Resend. Its mail driver is a plain JSON-over-HTTPS
1155
1173
  sender that posts \`{from, to, subject, text, html, reply_to}\` with a bearer
@@ -1158,8 +1176,10 @@ it needs no adapter. Any provider with the same shape works: set
1158
1176
  \`MAIL_HTTP_ENDPOINT\`, \`MAIL_HTTP_TOKEN\` and \`MAIL_DRIVER=http\` in the
1159
1177
  project's environment settings, and set the first two **together** — either
1160
1178
  one on its own stands the Resend bridge down, so a key issued for Resend is
1161
- never presented to an endpoint you chose. Only \`RESEND_API_KEY\` turns the
1162
- driver on by itself. Delete it once you have moved off Resend.
1179
+ never presented to an endpoint you chose. Setting \`MAIL_DRIVER\` to anything
1180
+ but \`http\` stands the bridge down too, for the same reason: a board that
1181
+ moved to SMTP must not send through its new provider from Resend's domain.
1182
+ Delete \`RESEND_API_KEY\` once you have moved off Resend.
1163
1183
 
1164
1184
  Check it worked: sign in as the administrator and use the test button on
1165
1185
  **/admin → Settings → Mail**.
@@ -1183,15 +1203,23 @@ drains; nothing here runs it on its own, because there is no worker process on
1183
1203
  a function platform. Two things about it are worth knowing **before** you
1184
1204
  deploy rather than after:
1185
1205
 
1186
- - **A per-minute schedule needs a paid plan.** Hobby allows a couple of cron
1187
- jobs and runs each of them roughly once a day, at an hour Vercel chooses;
1188
- only paid plans accept an arbitrary cron expression. A board ticking daily
1189
- still loses nothing tasks are written so a missed run delays work rather
1190
- than dropping it but "as it happens" notifications become a daily digest in
1191
- all but name. To keep a minute-by-minute tick on Hobby, drive
1192
- \`${TICK_PATH}\` from something else that can call a URL on a schedule — a
1193
- GitHub Actions workflow, a systemd timer, an uptime pinger — presenting
1194
- \`TICK_SECRET\` instead.
1206
+ - **This ships a daily schedule, because Hobby refuses anything faster.** A
1207
+ Hobby plan rejects a cron expression that would run more than once a day
1208
+ the deployment fails outright rather than being slowed down so
1209
+ \`vercel.json\` carries \`${TICK_SCHEDULE}\` and deploys anywhere. On a
1210
+ paid plan, edit it to \`* * * * *\` and the board ticks every minute.
1211
+
1212
+ A daily tick loses nothing permanently: tasks are written so a missed run
1213
+ delays work rather than dropping it, and a password reset is sent as the
1214
+ request is handled rather than waiting for a tick. What it does delay is
1215
+ everything the tick drives — a new post is not findable in search, and a
1216
+ notification is not sent, until the next run.
1217
+
1218
+ **To keep a fast tick without paying**, drive \`${TICK_PATH}\` from anything
1219
+ that can call a URL on a schedule — a GitHub Actions workflow, a systemd
1220
+ timer, an uptime pinger — presenting \`TICK_SECRET\` instead of
1221
+ \`CRON_SECRET\`. The endpoint accepts either, so the Vercel cron and an
1222
+ outside scheduler can both drive it.
1195
1223
  - **\`maxDuration = 300\` is validated when the project builds, not when the
1196
1224
  function runs.** A plan that does not allow 300 seconds therefore **fails the
1197
1225
  deployment** rather than clamping the request. With Fluid Compute — the