create-meith 0.37.3 → 0.37.4

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
@@ -460,7 +460,7 @@ save-exact=true
460
460
  *
461
461
  * Adding a theme is: \`npm install\` it, add a line here, redeploy. Adding a
462
462
  * plugin is the same, through board.plugins.json and meith.plugins.ts \u2014
463
- * see docs/customization/plugins.md.
463
+ * see docs/extensions/plugins.md.
464
464
  */
465
465
  import { defineForumConfig } from '@meith/web/config'
466
466
  import {
@@ -500,7 +500,7 @@ export default defineForumConfig({
500
500
  // them. A plugin that does not fit that convention can be added here by hand instead \u2014
501
501
  // keep it out of board.plugins.json so a regenerate does not drop it.
502
502
  //
503
- // docs/customization/plugins.md explains both.
503
+ // docs/extensions/plugins.md explains both.
504
504
 
505
505
  import type { InstalledPlugin } from '@meith/web/config'
506
506
 
@@ -635,7 +635,7 @@ jobs:
635
635
  # \`npm install\` below), so a build here is heavier than \`Dockerfile.prebuilt\`'s
636
636
  # thin delta \u2014 that image, pulled rather than built, is the trade the advanced
637
637
  # path takes for a low-spec build server or a faster deploy (see \`README.md\`
638
- # and, in the meith repository, docs/getting-started/deployment/docker-compose.md,
638
+ # and, in the meith repository, docs/operations/docker-compose.md,
639
639
  # "Custom boards").
640
640
  #
641
641
  # Two stages, not three: unlike the official image, this does not prune down
@@ -733,7 +733,7 @@ ENTRYPOINT ["./docker-entrypoint.sh"]
733
733
  #
734
734
  # FROM the published framework base image \u2014 deps + framework layers only,
735
735
  # locked to this exact release (see the meith repository's
736
- # docs/getting-started/deployment/docker-compose.md, "Custom boards", and docker/Dockerfile.base for what
736
+ # docs/operations/docker-compose.md, "Custom boards", and docker/Dockerfile.base for what
737
737
  # it is and is not). This board's own Dockerfile only ever installs its own
738
738
  # delta on top of it \u2014 a new plugin's own dependency, typically nothing more
739
739
  # \u2014 which is what keeps a rebuild after \`npm install some-plugin\` a matter
@@ -1072,7 +1072,7 @@ services:
1072
1072
  # service the variables the file names, so a Scheduled Task running
1073
1073
  # \`meith backup\` in this container would never see them without these
1074
1074
  # lines \u2014 see the meith repository's
1075
- # docs/getting-started/deployment/coolify.md, "Set up backups".
1075
+ # docs/operations/coolify.md, "Set up backups".
1076
1076
  - BACKUP_S3_BUCKET=\${BACKUP_S3_BUCKET:-}
1077
1077
  - BACKUP_S3_REGION=\${BACKUP_S3_REGION:-}
1078
1078
  - BACKUP_S3_ACCESS_KEY_ID=\${BACKUP_S3_ACCESS_KEY_ID:-}
@@ -1098,7 +1098,7 @@ services:
1098
1098
  # @meith/worker is not published (see the meith repository's
1099
1099
  # docs/contributing/release.md), so there is no compiled worker binary a scaffolded
1100
1100
  # board can run \u2014 this drives the tick the alternative way the meith
1101
- # repository documents in docs/getting-started/deployment/docker-compose.md, "Running the tick without
1101
+ # repository documents in docs/operations/docker-compose.md, "Running the tick without
1102
1102
  # a second set of credentials": a small loop calling /api/system/tick.
1103
1103
  worker:
1104
1104
  image: alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
@@ -1244,7 +1244,7 @@ services:
1244
1244
  # service the variables the file names, so a Scheduled Task running
1245
1245
  # \`meith backup\` in this container would never see them without these
1246
1246
  # lines \u2014 see the meith repository's
1247
- # docs/getting-started/deployment/coolify.md, "Set up backups".
1247
+ # docs/operations/coolify.md, "Set up backups".
1248
1248
  - BACKUP_S3_BUCKET=\${BACKUP_S3_BUCKET:-}
1249
1249
  - BACKUP_S3_REGION=\${BACKUP_S3_REGION:-}
1250
1250
  - BACKUP_S3_ACCESS_KEY_ID=\${BACKUP_S3_ACCESS_KEY_ID:-}
@@ -1270,7 +1270,7 @@ services:
1270
1270
  # @meith/worker is not published (see the meith repository's
1271
1271
  # docs/contributing/release.md), so there is no compiled worker binary a scaffolded
1272
1272
  # board can run \u2014 this drives the tick the alternative way the meith
1273
- # repository documents in docs/getting-started/deployment/docker-compose.md, "Running the tick without
1273
+ # repository documents in docs/operations/docker-compose.md, "Running the tick without
1274
1274
  # a second set of credentials": a small loop calling /api/system/tick.
1275
1275
  worker:
1276
1276
  image: alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
@@ -1308,7 +1308,7 @@ volumes:
1308
1308
  # secrets for you: every value Coolify would have filled in \u2014 the database
1309
1309
  # password, AUTH_SECRET, TICK_SECRET, the board's own address \u2014 comes from
1310
1310
  # a \`.env\` beside this file instead. See the meith repository's
1311
- # docs/getting-started/deployment/docker-compose.md, which this file is
1311
+ # docs/operations/docker-compose.md, which this file is
1312
1312
  # the last step of.
1313
1313
  #
1314
1314
  # \`docker compose\` only auto-discovers a file literally named
@@ -1324,7 +1324,7 @@ volumes:
1324
1324
  # to GitHub and let \`.github/workflows/build.yml\` do it, or run
1325
1325
  # \`docker build -f Dockerfile.prebuilt ...\` by hand) and change the two
1326
1326
  # \`build: .\` lines below to \`image: <that image>:<version>\` \u2014 the
1327
- # substitution docs/getting-started/deployment/docker-compose.md, "Building
1327
+ # substitution docs/operations/docker-compose.md, "Building
1328
1328
  # somewhere else", walks through.
1329
1329
  services:
1330
1330
  postgres:
@@ -1406,7 +1406,7 @@ services:
1406
1406
  APP_URL: \${APP_URL:-http://localhost:3000}
1407
1407
  # One reverse proxy (Caddy, in the guide's own walkthrough) sits in
1408
1408
  # front of \`web\` \u2014 see "Count your proxies" in
1409
- # docs/getting-started/deployment/docker-compose.md.
1409
+ # docs/operations/docker-compose.md.
1410
1410
  TRUSTED_PROXY_HOPS: \${TRUSTED_PROXY_HOPS:-1}
1411
1411
  # Leaving this at \`log\` does not mean no mail: it means the board
1412
1412
  # decides, from the installer on first run or from
@@ -1469,7 +1469,7 @@ services:
1469
1469
  depends_on:
1470
1470
  - web
1471
1471
 
1472
- # Off by default \u2014 see docs/guides/operations/scaling.md before setting
1472
+ # Off by default \u2014 see docs/operations/scaling.md before setting
1473
1473
  # CACHE_DRIVER=redis above. This is the server it needs.
1474
1474
  redis:
1475
1475
  profiles: ['redis']
@@ -1498,264 +1498,91 @@ volumes:
1498
1498
  "README.md",
1499
1499
  `# ${name}
1500
1500
 
1501
- A forum, built on [Meith](${repositoryUrl}).
1502
-
1503
- ## Deploy
1504
-
1505
- Three paths onto a server, all ending at the same \`/install\`. **Quick
1506
- start** onto [Coolify](https://coolify.io) is the default and needs nothing
1507
- but a push; **advanced/prebuilt** moves the build off the server, onto
1508
- GitHub Actions, for a low-spec build server or a faster deploy; **without a
1509
- panel** is the same four containers run by hand, with your own \`.env\` and
1510
- reverse proxy, and no Coolify at all. Pick one \u2014 a board only ever runs one
1511
- of them at a time.
1512
-
1513
- ### Quick start (default)
1514
-
1515
- Coolify builds the image itself, from this repository, every time it
1516
- deploys \u2014 there is nothing to push anywhere first and no image tag to paste
1517
- in. Two steps:
1518
-
1519
- 1. **Push this repository to GitHub.**
1520
-
1521
- 2. **Point Coolify at \`docker-compose.yaml\`** \u2014 a **Public Git repository**
1522
- resource with **Docker Compose** as its build pack, this repository as its
1523
- source. The name is Coolify's own default, so its **Compose file** field is
1524
- already right when the form opens, and the file already carries Coolify's
1525
- own "magic variables" for \`AUTH_SECRET\`, \`TICK_SECRET\` and the database
1526
- password, generated on the first deploy and never typed in. Nothing else to
1527
- set: \`docker-compose.yaml\` builds \`web\` and \`migrate\` from \`Dockerfile\`
1528
- itself, so there is no \`MEITH_IMAGE\` here at all.
1529
-
1530
- 3. **Deploy, then \`/install\` on your own domain.** Coolify issues the
1531
- certificate; the installer from there is the one
1532
- [docs/getting-started/deployment/coolify.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/coolify.md#4-run-the-installer)
1533
- walks through, screen for screen. It seals itself when it finishes, and
1534
- \`/install\` answers 404 from then on \u2014 run it **against the database you
1535
- are going to keep**. Every push to \`main\` after this is picked up the next
1536
- time Coolify's own **Redeploy** button runs \u2014 pushing alone does not
1537
- rebuild it.
1538
-
1539
- The trade for that zero setup is a heavier build: \`Dockerfile\` installs this
1540
- board's full dependency closure on the server itself, on every deploy, rather
1541
- than starting from a warm base image. A 2 GB VPS can OOM on it. If that is
1542
- your server, use the advanced path below instead.
1543
-
1544
- A quick-start board never needs \`Dockerfile.prebuilt\`,
1545
- \`docker-compose.prebuilt.yaml\` or \`.github/workflows/build.yml\` \u2014 delete all
1546
- three.
1547
-
1548
- ### Advanced / prebuilt \u2014 for a low-spec server or a faster deploy
1549
-
1550
- Something else builds the image ahead of time; the server only ever pulls
1551
- one. Three steps, nothing to configure by hand beyond one value only you know:
1552
-
1553
- 1. **Push this repository to GitHub.** \`.github/workflows/build.yml\` builds
1554
- \`Dockerfile.prebuilt\` on every push to \`main\` and pushes the result to your
1555
- own GitHub Container Registry, \`ghcr.io/<you>/${name}\` \u2014 using only the
1556
- \`GITHUB_TOKEN\` every GitHub Actions run already carries. No secret to
1557
- add, no registry account beyond the GitHub account you already have.
1558
-
1559
- That build is the thing step 2 waits on: open the repository's
1560
- **Actions** tab and let the run finish, because its **Summary** is where
1561
- the exact image to paste into step 2 comes from. The Summary also links
1562
- the package itself, to check it is public \u2014 a build from a public
1563
- repository usually lands public already, and a private one fails
1564
- Coolify's pull with an authentication error no operator can act on.
1565
-
1566
- 2. **Point Coolify at \`docker-compose.prebuilt.yaml\`** \u2014 a
1567
- **Public Git repository** resource with **Docker Compose** as its build
1568
- pack, this repository as its source, and its **Compose file** field
1569
- changed from Coolify's default of \`docker-compose.yaml\` to
1570
- \`docker-compose.prebuilt.yaml\`. That file carries Coolify's own "magic
1571
- variables" for \`AUTH_SECRET\`, \`TICK_SECRET\` and the database password,
1572
- generated on the first deploy and never typed in. The one thing Coolify
1573
- cannot generate is the image step 1 just pushed: set \`MEITH_IMAGE\` in the
1574
- resource's own environment to one of the two values that run's Summary
1575
- printed (\`docker-compose.prebuilt.yaml\` refuses to start without it, with
1576
- a message saying why). \`ghcr.io/<you>/${name}:\${{ github.sha }}\` names
1577
- that one build and nothing else, ever; \`ghcr.io/<you>/${name}:latest\`
1578
- follows \`main\` instead, so installing a plugin later is a push and a
1579
- **Redeploy** \u2014 the trade this path takes, at the cost of an unrelated
1580
- redeploy pulling whatever \`main\` most recently built.
1581
-
1582
- 3. **Deploy, then \`/install\` on your own domain.** Same installer, same
1583
- [docs/getting-started/deployment/coolify.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/coolify.md#4-run-the-installer)
1584
- walk-through, same one-time seal. Every push to \`main\` after this rebuilds
1585
- the image; Coolify's own **Redeploy** button is what actually pulls it \u2014
1586
- pushing alone does not.
1587
-
1588
- No Docker Hub, no paid CI: GitHub Actions' free tier and GHCR are the whole
1589
- build side of this, for a board of any size.
1590
-
1591
- **Building it yourself**: works on any machine with Docker, if you would
1592
- rather not use GitHub Actions for the build \u2014 push the result wherever
1593
- \`docker-compose.prebuilt.yaml\`'s \`MEITH_IMAGE\` can reach.
1501
+ A community board built on [Meith](${repositoryUrl}).
1594
1502
 
1595
- \`\`\`sh
1596
- docker build -f Dockerfile.prebuilt --build-arg MEITH_VERSION=$(node -p "require('./package.json').dependencies['@meith/web']") -t ${name} .
1597
- \`\`\`
1503
+ ## Choose a deployment
1598
1504
 
1599
- ### Without a panel
1505
+ | Route | File | Where the image builds |
1506
+ |---|---|---|
1507
+ | Quick start with Coolify | \`docker-compose.yaml\` | Your server |
1508
+ | Advanced / prebuilt | \`docker-compose.prebuilt.yaml\` | GitHub Actions or another build machine |
1509
+ | Docker Compose without a panel | \`docker-compose.byhand.yaml\` | Your server |
1600
1510
 
1601
- \`docker-compose.byhand.yaml\`, beside the two Coolify files above, is the same
1602
- four containers deployed with nothing generating secrets for you: a \`.env\`
1603
- you write yourself, a port published for the reverse proxy you already run,
1604
- and \`docker compose up -d --build\` in place of a panel's Deploy button.
1605
- [docs/getting-started/deployment/docker-compose.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/docker-compose.md)
1606
- is the full walkthrough this file is the last step of, including the
1607
- \`.env\` this repository does not carry \u2014 nothing here belongs in git. Delete
1608
- this file if you know you will only ever deploy through Coolify; keep it,
1609
- and it needs nothing else changed, if you later want to move away from
1610
- Coolify without changing how the board itself is built.
1511
+ Use one route for a deployment. The [Coolify guide](${repositoryUrl}/blob/main/docs/operations/coolify.md) and [Docker Compose guide](${repositoryUrl}/blob/main/docs/operations/docker-compose.md) cover prerequisites, secrets, domains and recovery.
1611
1512
 
1612
- Two things nothing configures for you, on any path:
1513
+ ### Quick start with Coolify
1613
1514
 
1614
- - **Mail.** Until \`MAIL_DRIVER\` and its three settings exist, every message is
1615
- written to the log and delivered to nobody, so password reset fails silently.
1616
- - **The tick.** The compose file's \`worker\` service drives it here \u2014 a small
1617
- loop calling \`/api/system/tick\` once a minute, since \`@meith/web\`'s own
1618
- worker package is not something a board outside the meith monorepo can
1619
- depend on yet. Deploy some other way and something still has to call that
1620
- route (or run \`meith task:run\`) every minute, or nothing catches up
1621
- and nothing errors.
1515
+ 1. Push this repository to GitHub.
1516
+ 2. Create a Git repository resource in Coolify with the Docker Compose build pack and \`/docker-compose.yaml\` as the Compose file.
1517
+ 3. Assign the board's domain and deploy. Coolify supplies database and authentication secrets; save a protected recovery copy.
1518
+ 4. Confirm \`postgres\` is healthy, \`migrate\` exits successfully, and \`web\` and \`worker\` run.
1519
+ 5. Open \`/install\`, unlock with \`AUTH_SECRET\`, and create the board and its first administrator. The installer seals itself and returns 404 after completion.
1622
1520
 
1623
- ## Local
1521
+ A push alone does not rebuild this route. Use Coolify's **Redeploy** after pushing. If the server cannot complete the build, use the prebuilt route.
1624
1522
 
1625
- \`\`\`sh
1626
- npm install
1627
- npm run dev
1628
- \`\`\`
1523
+ ### Advanced / prebuilt
1629
1524
 
1630
- No environment file, no database: with no \`DATABASE_URL\` the board serves
1631
- deterministic in-memory sample data, which is enough to click through every
1632
- reading surface.
1525
+ 1. Let \`.github/workflows/build.yml\` finish in GitHub Actions. It builds and publishes your board's image.
1526
+ 2. Make that image accessible to Coolify and select \`/docker-compose.prebuilt.yaml\`.
1527
+ 3. Set \`MEITH_IMAGE\` to the exact image from the workflow summary. The commit tag uses \`\${{ github.sha }}\`; \`:latest\` follows later builds and can change on redeploy.
1528
+ 4. Deploy and complete \`/install\` as above.
1633
1529
 
1634
- Posting needs Postgres. Copy \`.env.example\` to \`.env.local\`, set
1635
- \`DATABASE_URL\` and the two secrets in it, then:
1530
+ For a local image build, the build argument comes from the board's pinned package:
1636
1531
 
1637
1532
  \`\`\`sh
1638
- npm run meith -- migrate
1639
- echo "<password>" | npm run meith -- user:create --username <name> --email <address> --group administrators
1533
+ docker build -f Dockerfile.prebuilt --build-arg MEITH_VERSION=$(node -p "require('./package.json').dependencies['@meith/web']") -t ${name} .
1640
1534
  \`\`\`
1641
1535
 
1642
- ## Configuring
1536
+ After changing the board, wait for its new image and update \`MEITH_IMAGE\` if pinned to a commit, then redeploy.
1643
1537
 
1644
- - **\`meith.config.ts\`** \u2014 installed themes and plugins. Everything installable
1645
- is named here so the bundler can see it; nothing is found by scanning a
1646
- directory at runtime.
1647
- - **\`/admin\`** \u2014 settings, forums, groups, members, themes, maintenance. An
1648
- administrator re-enters their password to get in, and again for anything
1649
- destructive.
1650
- - **\`npm run meith -- --help\`** \u2014 the operator CLI. Everything the panel does
1651
- and a few things it cannot, without a browser.
1538
+ ## Run locally
1652
1539
 
1653
- ## Installing plugins and themes
1540
+ \`\`\`sh
1541
+ npm install
1542
+ npm run dev
1543
+ \`\`\`
1654
1544
 
1655
- Nothing installs into a running container \u2014 a plugin or theme has to be
1656
- built into the image. In this repository:
1545
+ Open \`http://localhost:3000\`. Without \`DATABASE_URL\`, this is a read-only fixture preview. For persistent registration and posting, follow [Create a writable local board](${repositoryUrl}/blob/main/docs/operations/local-board.md).
1657
1546
 
1658
- 1. **Add it.** A **plugin** is one command, which installs the package and
1659
- registers it:
1547
+ ## Configure the community
1660
1548
 
1661
- \`\`\`sh
1662
- npm run meith -- plugin:add @meith/plugin-dues
1663
- \`\`\`
1549
+ - \`meith.config.ts\` registers themes and board configuration.
1550
+ - \`board.plugins.json\` and \`meith.plugins.ts\` register installed plugins.
1551
+ - \`/admin\` manages forums, members, permissions and settings.
1552
+ - \`npm run meith -- --help\` lists operator commands.
1664
1553
 
1665
- It writes \`board.plugins.json\` and regenerates \`meith.plugins.ts\` for you
1666
- (\`npm run meith -- plugin:remove <key>\` reverses it). A **theme** is
1667
- \`npm install --save-exact @meith/theme-midnight\`, then an entry in
1668
- \`meith.config.ts\`'s \`themes\` map following the shape of the \`default\` one
1669
- already there \u2014 set \`defaultTheme\` to its key to make it the board's
1670
- default.
1554
+ Before inviting members, [test email delivery](${repositoryUrl}/blob/main/docs/operations/mail.md), verify [scheduled work](${repositoryUrl}/blob/main/docs/operations/scheduled-tasks.md), and [configure backups](${repositoryUrl}/blob/main/docs/operations/backups.md). The log mail driver delivers nothing. This deployment's worker calls the web application's tick endpoint. For a manual development run, use \`npm run meith -- task:run\`.
1671
1555
 
1672
- 2. **Commit and push**, then **Redeploy** from Coolify \u2014 pushing alone does
1673
- not rebuild. Quick start builds the new image on that redeploy; advanced/prebuilt
1674
- waits for \`.github/workflows/build.yml\` to finish first, and Redeploy is
1675
- what actually pulls the result.
1556
+ ## Install an extension
1676
1557
 
1677
- 3. **If it ships database changes, apply them once it is up** \u2014 from
1678
- **Admin \u2192 System** (**Version & migrations**) in the browser, or:
1558
+ Add a plugin from this checkout:
1679
1559
 
1680
- \`\`\`sh
1681
- docker compose run --rm web meith upgrade
1682
- \`\`\`
1560
+ \`\`\`sh
1561
+ npm run meith -- plugin:add @meith/plugin-dues
1562
+ \`\`\`
1683
1563
 
1684
- See [Installing plugins and themes](${repositoryUrl}/blob/main/docs/customization/installing.md)
1685
- for the full guide.
1564
+ Commit the package and registry changes, build and deploy, then apply plugin migrations with \`meith upgrade\` against the deployed board. Follow [Install plugins and themes](${repositoryUrl}/blob/main/docs/operations/installing.md) for the full procedure and theme registration. Installing a package into a running container does not make it part of the next deployment.
1686
1565
 
1687
1566
  ## Upgrading
1688
1567
 
1689
- \`.github/workflows/update.yml\` does this for you: once a week \u2014 and
1690
- whenever you press **Run workflow** on the Actions tab \u2014 it checks for a new
1691
- Meith release and opens a pull request that moves every \`@meith/*\` package
1692
- and \`next\` together, and rewrites the deploy files this scaffold owns
1693
- (\`Dockerfile\`, the compose files, the workflows) to the new release's
1694
- shape. A file you have edited yourself is never rewritten; the run's log
1695
- names any it left for you. One-time setup: under
1696
- **Settings \u2192 Actions \u2192 General**, enable **Allow GitHub Actions to create
1697
- and approve pull requests**, or the workflow cannot open one.
1568
+ \`.github/workflows/update.yml\` checks weekly and opens an update pull request. It also supports **Run workflow**. Enable **Allow GitHub Actions to create and approve pull requests** under **Settings \u2192 Actions \u2192 General**.
1698
1569
 
1699
- Merging that pull request is still an upgrade, not a formality: read the
1700
- release notes it links, take a backup first, and press **Redeploy** in
1701
- Coolify after the merge \u2014 pushing alone does not rebuild. Once the new
1702
- version serves, run \`npm run meith -- upgrade\` against it for the plugin
1703
- migrations.
1570
+ Review the release notes, take a backup, and inspect any scaffold files the updater left for manual reconciliation. Merge, rebuild and redeploy; then run \`meith upgrade\` for plugin migrations. Core migrations run through the deployment's migration service. Migrations are forward-only; recovery uses a backup.
1704
1571
 
1705
- The same update, by hand and without waiting for the schedule:
1572
+ To prepare the update locally:
1706
1573
 
1707
1574
  \`\`\`sh
1708
1575
  npx create-meith@latest update
1709
- git commit -am "Update Meith"
1710
- git push
1711
1576
  \`\`\`
1712
1577
 
1713
- Under the hood, the version move is these two commands, plus the deploy-file
1714
- rewrite neither of them can do:
1578
+ The updater moves package pins and supported deployment files together. Its package update includes these commands; running them alone does not update deployment files:
1715
1579
 
1716
1580
  \`\`\`sh
1717
1581
  npm install --save-exact @meith/web@latest @meith/cli@latest @meith/theme-default@latest
1718
1582
  npm install --save-exact next@$(node -p "require('./node_modules/@meith/web/package.json').dependencies.next")
1719
1583
  \`\`\`
1720
1584
 
1721
- The second command is not optional. This board pins \`next\` itself, and
1722
- the npm commands alone never bump it: upgrading only the \`@meith/*\` packages
1723
- leaves the board's own pin on the old Next while \`@meith/web\` depends on the
1724
- new one, which npm resolves by installing both \u2014 the build then runs on one
1725
- version while everything reading \`package.json\` sees the other. Reading the
1726
- version out of the freshly installed \`@meith/web\` is what keeps the two the
1727
- same without anybody having to know the number.
1728
-
1729
- \`next\` and \`@meith/web\` move together or not at all, which is why one
1730
- updater owns the whole move and no dependency bot bumps either on its own.
1731
- What Dependabot *does* keep current is this repository's own GitHub Actions \u2014
1732
- \`.github/dependabot.yml\` opens a weekly pull request bumping the actions
1733
- pinned under \`.github/workflows\`, a safe, independent update the updater
1734
- leaves to it.
1735
-
1736
- On the quick-start path there is no version to keep in sync by hand:
1737
- \`Dockerfile\` runs \`npm install\` straight from this \`package.json\` on every
1738
- build, so a rebuild always picks up whatever is pinned there. On the
1739
- advanced/prebuilt path, that \`package.json\` change is the whole pin:
1740
- \`Dockerfile.prebuilt\`'s own \`FROM\` line takes the version as a build argument,
1741
- and \`.github/workflows/build.yml\` reads it straight out of \`package.json\`'s
1742
- own \`@meith/web\` dependency when it rebuilds \u2014 nothing in
1743
- \`Dockerfile.prebuilt\` itself to keep in sync by hand. \`--save-exact\` matters
1744
- either way: npm's default \`save-prefix\` is \`^\`, and a caret range is not a
1745
- legal Docker image tag for the advanced path \u2014 without it, this exact command
1746
- would write \`"^0.18.0"\` and the next \`Dockerfile.prebuilt\` build would fail
1747
- with \`invalid reference format\` instead of building. This
1748
- project's own \`.npmrc\` sets \`save-exact=true\` for the same reason, so an
1749
- \`npm install\` of anything else here \u2014 a plugin, say \u2014 stays pinned too; the
1750
- build workflow also refuses to build from anything but an exact version, as
1751
- a second line of defense. Once the rebuilt image is deployed, run
1752
- \`npm run meith -- upgrade\` against it for the plugin migrations \u2014 see
1753
- [the operator CLI](${repositoryUrl}/blob/main/docs/guides/operations/operating.md#the-operator-cli)
1754
- for running it against this deployment.
1755
-
1756
- Migrations are forward-only. Recovery is by restore, so take a backup first \u2014
1757
- there is no down migration to undo a destructive one, and a button that pretended
1758
- otherwise would be worse than its absence.
1585
+ Keep Next.js aligned with \`@meith/web\`. Use \`--save-exact\`: a caret range is not a legal Docker image tag. Read [Upgrade Meith](${repositoryUrl}/blob/main/docs/operations/upgrading.md) before applying the change.
1759
1586
  `
1760
1587
  );
1761
1588
  if (target === "vercel") {
@@ -1792,277 +1619,95 @@ ${AT_ROOT_IGNORES}
1792
1619
  function vercelReadme({ name, repositoryUrl, templateRepositoryUrl }) {
1793
1620
  return `# ${name}
1794
1621
 
1795
- A forum, built on [Meith](${repositoryUrl}), running as Vercel functions.
1622
+ A community board built on [Meith](${repositoryUrl}), deployed as Vercel functions.
1796
1623
 
1797
1624
  [![Deploy with Vercel](https://vercel.com/button)](${deployButtonUrl(templateRepositoryUrl)})
1798
1625
 
1799
- ## What the button provisions
1800
-
1801
- - **A copy of this repository** under your own GitHub account. Vercel builds
1802
- from it, and every later push to \`main\` redeploys.
1803
- - **A Neon Postgres database**, attached to the project. Neon publishes the
1804
- pooled connection string as \`DATABASE_URL\` and the direct one as
1805
- \`DATABASE_URL_UNPOOLED\`.
1806
- - **An Upstash Redis store**, attached the same way, for the shared cache. It
1807
- publishes \`KV_URL\`, which the board reads as \`REDIS_URL\` \u2014 \`KV_REST_API_URL\`
1808
- beside it is an HTTPS endpoint and is not used for this.
1809
- - **A Vercel Blob store** for uploads, which publishes \`BLOB_STORE_ID\` into the
1810
- project by itself. That is the whole credential: the board hands the id to
1811
- Vercel's SDK, which authenticates with the deployment's own OIDC identity, so
1812
- there is no token to copy. This is what used to be four hand-typed \`S3_*\`
1813
- secrets.
1814
- - **A Resend mail account**, attached the same way, which publishes
1815
- \`RESEND_API_KEY\` and \`RESEND_EMAIL_DOMAIN\`. The board reads both names
1816
- directly: its mail driver already speaks Resend's request shape, so there is
1817
- nothing to adapt, and the sending domain is what the sender is built from.
1818
- - **A Vercel project** carrying \`vercel.json\` \u2014 the build command
1819
- \`${VERCEL_BUILD_COMMAND}\`,
1820
- which applies the schema before it builds, materializes the board's app at
1821
- the project root so the artefact lands where Vercel reads it, and the cron
1822
- entry that drives the tick.
1823
-
1824
- **Mail needs no variables after the deploy.** Resend publishes both its key
1825
- and its sending domain, and the board sends from \`${RESEND_SENDER_MAILBOX}@\`
1826
- that domain \u2014 see *Mail* below to send from a different address, and for the
1827
- one case that does need you: a domain Resend has not verified yet.
1828
-
1829
- ## What to type into the deploy form
1830
-
1831
- **Two secrets**, generated rather than chosen. Thirty-two characters is a floor
1832
- the board enforces at boot, not a suggestion:
1626
+ ## 1. Connect services and set secrets
1627
+
1628
+ The deployment template requests Neon PostgreSQL, Upstash Redis, Vercel Blob and Resend. Keep access to these service accounts and review their current limits and pricing.
1629
+
1630
+ Generate two independent secrets:
1833
1631
 
1834
1632
  \`\`\`sh
1835
- openssl rand -hex 32 # AUTH_SECRET
1836
- openssl rand -hex 32 # CRON_SECRET
1633
+ openssl rand -hex 32
1634
+ openssl rand -hex 32
1837
1635
  \`\`\`
1838
1636
 
1839
- \`CRON_SECRET\` is the name Vercel Cron sends, as \`Authorization: Bearer\`, and it
1840
- cannot be told to send another \u2014 the caller is the platform, so this one has to
1841
- be an environment variable both ends can read, and cannot be something the
1842
- board makes up for itself. Note that this floor is stricter than the 16
1843
- characters Vercel's own cron documentation suggests \u2014 a value generated by
1844
- following those instructions is refused here, and the fix is a longer secret.
1637
+ Use them for \`AUTH_SECRET\` and \`CRON_SECRET\`. Each must be at least 32 characters. Keep a recovery copy of the original \`AUTH_SECRET\`; it seals stored secrets.
1845
1638
 
1846
- \`AUTH_SECRET\` seals members' two-factor secrets and signs the unsubscribe links
1847
- in outgoing mail. It stays in the environment deliberately: a copy of the
1848
- database is then not enough to forge either.
1849
-
1850
- **That is the whole form.** Everything else the board works out from the stores
1851
- this button just linked to the project:
1639
+ The platform derives the following defaults when you have not supplied explicit overrides:
1852
1640
 
1853
1641
  \`\`\`ini
1854
1642
  ${VERCEL_DERIVED_DRIVERS.join("\n")}
1855
1643
  \`\`\`
1856
1644
 
1857
- \`DIRECT_DATABASE_URL\` comes from Neon's own \`DATABASE_URL_UNPOOLED\`, or
1858
- \`POSTGRES_URL_NON_POOLING\` if that one is absent \u2014 migrations and the first-run
1859
- installer each hold a session-level advisory lock, which the pooled
1860
- \`DATABASE_URL\` cannot hold. \`REDIS_URL\` comes from Upstash's \`KV_URL\`, the one
1861
- variable it publishes that speaks the Redis protocol.
1862
-
1863
- Every one of those derivations is scoped to this platform, fires only where you
1864
- have not set the variable yourself, and **refuses to boot rather than guess**.
1865
- If a store is missing, or publishes a name this board does not know, the deploy
1866
- stops with a message naming every variable it looked at \u2014 it will not fall back
1867
- to caching inside each instance, or to uploads on a disk that is discarded with
1868
- the instance. When the name is one we do not know, set \`REDIS_URL\` or
1869
- \`DIRECT_DATABASE_URL\` in the project's environment settings and the derivation
1870
- stands aside.
1871
-
1872
- If you would rather keep uploads somewhere you hold yourself \u2014 see *Leaving
1873
- Vercel* below for why that matters \u2014 set \`FILESTORE_DRIVER=s3\` and add
1874
- \`S3_BUCKET\`, \`S3_REGION\`, \`S3_ACCESS_KEY_ID\` and \`S3_SECRET_ACCESS_KEY\` in the
1875
- project's environment settings, with \`S3_ENDPOINT\` for a bucket that is not AWS
1876
- (\`S3_REGION=auto\` for R2). The same board runs either way.
1645
+ | Service value | Meith uses it for |
1646
+ |---|---|
1647
+ | \`DATABASE_URL\` | Runtime database connection |
1648
+ | \`DATABASE_URL_UNPOOLED\`, falling back to \`POSTGRES_URL_NON_POOLING\` | \`DIRECT_DATABASE_URL\` for migrations and installer locks |
1649
+ | \`KV_URL\` | \`REDIS_URL\`; the Redis protocol connection, not the HTTP REST endpoint |
1650
+ | \`BLOB_STORE_ID\` | Upload storage authenticated through the deployment identity |
1651
+ | \`RESEND_API_KEY\`, \`RESEND_EMAIL_DOMAIN\` | HTTP mail credentials and sender |
1652
+
1653
+ If a required service configuration is missing, Meith refuses to boot rather than guess. Inspect the named variables in the deployment log. See [Vercel configuration](${repositoryUrl}/blob/main/docs/operations/vercel-configuration.md) for explicit overrides.
1654
+
1655
+ ## 2. Deploy and install
1656
+
1657
+ The build command is \`${VERCEL_BUILD_COMMAND}\`. It applies core migrations before building. Keep preview deployments on a separate database if they must not migrate production.
1658
+
1659
+ When the deployment succeeds, open \`/install\`. Unlock with \`AUTH_SECRET\`, confirm the permanent public board address, and create the first administrator. Installation seals the route; \`/install\` then returns 404.
1877
1660
 
1878
1661
  ## Mail
1879
1662
 
1880
- **There is nothing to set.** The Resend the deploy form added publishes two
1881
- names into the project: \`RESEND_API_KEY\`, and \`RESEND_EMAIL_DOMAIN\` \u2014 the
1882
- domain it sends from. The board reads both, sends from
1883
- \`${RESEND_SENDER_MAILBOX}@\` that domain, and posts over Resend's HTTPS API.
1884
-
1885
- **If Resend refuses the messages**, that domain is not verified yet. Resend
1886
- will not send from a domain it has not verified, whoever set the address, so
1887
- verify it from the Resend dashboard \u2014 the deploy cannot do that step for you,
1888
- because it is Resend confirming you own the domain. This is the one thing here
1889
- that can need attention, and it announces itself: the test button below says
1890
- so rather than the board failing quietly.
1891
-
1892
- **To send from a different address**, set \`MAIL_FROM\` in the project's
1893
- environment settings and redeploy. It must be at a domain Resend has verified,
1894
- for the same reason. An address you set always wins over the derived one.
1895
-
1896
- A board with the key but no verified domain \u2014 which is what you get if you
1897
- remove the integration's \`RESEND_EMAIL_DOMAIN\` without putting a
1898
- \`MAIL_FROM\` in its place \u2014 does not guess a sender. It stays on the log
1899
- driver and delivers nothing, which is the honest outcome: a guessed sender at
1900
- an unverified domain would be refused by Resend anyway, one message at a
1901
- time.
1902
-
1903
- The board is not tied to Resend. Its mail driver is a plain JSON-over-HTTPS
1904
- sender that posts \`{from, to, subject, text, html, reply_to}\` with a bearer
1905
- token \u2014 Resend's \`POST /emails\` happens to be exactly that shape, which is why
1906
- it needs no adapter. Any provider with the same shape works: set
1907
- \`MAIL_HTTP_ENDPOINT\`, \`MAIL_HTTP_TOKEN\` and \`MAIL_DRIVER=http\` in the
1908
- project's environment settings, and set the first two **together** \u2014 either
1909
- one on its own stands the Resend bridge down, so a key issued for Resend is
1910
- never presented to an endpoint you chose. Setting \`MAIL_DRIVER\` to anything
1911
- but \`http\` stands the bridge down too, for the same reason: a board that
1912
- moved to SMTP must not send through its new provider from Resend's domain.
1913
- Delete \`RESEND_API_KEY\` once you have moved off Resend.
1914
-
1915
- Check it worked: sign in as the administrator and use the test button on
1916
- **/admin \u2192 Settings \u2192 Mail**.
1917
-
1918
- ## First run: \`/install\`
1919
-
1920
- The build applies migrations, but an empty schema is not yet a board. Open
1921
- \`https://<your-deployment>/install\` once the first deploy is green. It asks for
1922
- the board's name and address and for the first administrator's username, email
1923
- and password, creates the board and that account, and then **seals itself**:
1924
- \`/install\` answers 404 from then on. Run it against the database you intend to
1925
- keep \u2014 the screens are the ones
1926
- [docs/getting-started/deployment/docker-compose.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/docker-compose.md#6-install-it)
1927
- walks through.
1928
-
1929
- ## The tick
1930
-
1931
- \`vercel.json\` asks Vercel to call \`${TICK_PATH}\` on \`${TICK_SCHEDULE}\`. That
1932
- route is how bans expire, digests send, mail leaves the outbox and the queue
1933
- drains; nothing here runs it on its own, because there is no worker process on
1934
- a function platform. Two things about it are worth knowing **before** you
1935
- deploy rather than after:
1936
-
1937
- - **This ships a daily schedule, because Hobby refuses anything faster.** A
1938
- Hobby plan rejects a cron expression that would run more than once a day \u2014
1939
- the deployment fails outright rather than being slowed down \u2014 so
1940
- \`vercel.json\` carries \`${TICK_SCHEDULE}\` and deploys anywhere. On a
1941
- paid plan, edit it to \`* * * * *\` and the board ticks every minute.
1942
-
1943
- A daily tick loses nothing permanently: tasks are written so a missed run
1944
- delays work rather than dropping it, and a password reset is sent as the
1945
- request is handled rather than waiting for a tick. What it does delay is
1946
- everything the tick drives \u2014 a new post is not findable in search, and a
1947
- notification is not sent, until the next run.
1948
-
1949
- **To keep a fast tick without paying**, drive \`${TICK_PATH}\` from anything
1950
- that can call a URL on a schedule \u2014 a GitHub Actions workflow, a systemd
1951
- timer, an uptime pinger \u2014 presenting \`TICK_SECRET\` instead of
1952
- \`CRON_SECRET\`. The endpoint accepts either, so the Vercel cron and an
1953
- outside scheduler can both drive it.
1954
- - **\`maxDuration = 300\` is validated when the project builds, not when the
1955
- function runs.** A plan that does not allow 300 seconds therefore **fails the
1956
- deployment** rather than clamping the request. With Fluid Compute \u2014 the
1957
- default for new projects \u2014 Hobby allows 300 and this builds as written. With
1958
- Fluid Compute switched off, Hobby caps a function at 60 seconds and the build
1959
- fails. Turn Fluid Compute back on.
1960
-
1961
- A tick that reaches the tasks and runs them answers \`200\` even when one of them
1962
- threw, with \`ok: false\` and the failure named in \`ran\`. That is deliberate:
1963
- schedulers retry non-2xx answers, and a task that fails every time would turn
1964
- each retry into another attempt against whatever it is failing against.
1663
+ Mail needs no variables after the deploy when the Resend integration supplies both \`RESEND_API_KEY\` and \`RESEND_EMAIL_DOMAIN\`. The default sender is \`${RESEND_SENDER_MAILBOX}@\` followed by that domain. Verify the sending domain with Resend and send a test from **Admin \u2192 Settings \u2192 Mail**.
1664
+
1665
+ To send from a different address, set \`MAIL_FROM\` and redeploy. The address must be allowed by your provider. Missing sender configuration can leave mail on the log driver, which delivers nothing.
1666
+
1667
+ For another HTTP provider, set \`MAIL_DRIVER=http\`, \`MAIL_HTTP_ENDPOINT\`, \`MAIL_HTTP_TOKEN\` and \`MAIL_FROM\`; endpoint and token must be supplied together. See [Email configuration](${repositoryUrl}/blob/main/docs/operations/mail.md).
1668
+
1669
+ ## 3. Verify scheduled work and hosting limits
1670
+
1671
+ \`vercel.json\` calls \`${TICK_PATH}\` on \`${TICK_SCHEDULE}\`: once a day. That cadence can delay notifications, search indexing and queued work. Time-sensitive plugin work can miss its useful delivery window.
1672
+
1673
+ Choose a cadence supported by your current plan, or use an external scheduler authenticated with an independently generated \`TICK_SECRET\`. The endpoint accepts \`CRON_SECRET\` or \`TICK_SECRET\`. A paid plan may provide more scheduling options; check [Vercel cron limits](https://vercel.com/docs/cron-jobs/usage-and-pricing).
1674
+
1675
+ The tick declares \`maxDuration = 300\`. Check the project's Fluid Compute setting and [function duration limit](https://vercel.com/docs/functions/configuring-functions/duration) before deploying; an unsupported duration can fail deployment. Upload limits also come from the platform, regardless of the board's attachment settings.
1676
+
1677
+ Inspect task results, not only HTTP status: a tick can return 200 with \`ok: false\` and failed tasks in \`ran\`. Follow [Scheduled tasks](${repositoryUrl}/blob/main/docs/operations/scheduled-tasks.md) and test real posting, uploads and email before inviting members.
1965
1678
 
1966
1679
  ## Upgrading
1967
1680
 
1968
- \`.github/workflows/update.yml\` does this for you: once a week \u2014 and
1969
- whenever you press **Run workflow** on the Actions tab \u2014 it checks for a new
1970
- Meith release and opens a pull request that moves every \`@meith/*\` package
1971
- and \`next\` together. A file you have edited yourself is never rewritten; the
1972
- run's log names any it left for you. One-time setup: under
1973
- **Settings \u2192 Actions \u2192 General**, enable **Allow GitHub Actions to create and
1974
- approve pull requests**, or the workflow cannot open one. Read the release
1975
- notes the pull request links and take a backup before merging; Vercel
1976
- rebuilds on the merge, and the build command applies the new migrations
1977
- before it builds.
1681
+ \`.github/workflows/update.yml\` opens a weekly update pull request and supports **Run workflow**. Enable **Allow GitHub Actions to create and approve pull requests** under **Settings \u2192 Actions \u2192 General**.
1682
+
1683
+ Read the release notes and take a backup before merging. Vercel redeploys the merged code and applies core migrations during the build. Apply plugin migrations with the operator CLI using the deployment's environment. Migrations are forward-only.
1978
1684
 
1979
- The same update, by hand and without waiting for the schedule:
1685
+ To prepare the update locally:
1980
1686
 
1981
1687
  \`\`\`sh
1982
1688
  npx create-meith@latest update
1983
- git commit -am "Update Meith"
1984
- git push
1985
1689
  \`\`\`
1986
1690
 
1987
- Under the hood, the version move is these two commands:
1691
+ The updater also reconciles supported deployment files. Its package update keeps Meith and Next.js aligned:
1988
1692
 
1989
1693
  \`\`\`sh
1990
1694
  npm install --save-exact @meith/web@latest @meith/cli@latest @meith/theme-default@latest
1991
1695
  npm install --save-exact next@$(node -p "require('./node_modules/@meith/web/package.json').dependencies.next")
1992
1696
  \`\`\`
1993
1697
 
1994
- \`--save-exact\` matters and \`.npmrc\` already sets it for everything else
1995
- installed here.
1996
-
1997
- The second command is not optional. This board pins \`next\` itself \u2014 Vercel
1998
- reads that pin to pick its Next.js builder \u2014 and the npm commands alone never
1999
- bump it. Upgrading only the \`@meith/*\` packages leaves two versions of Next
2000
- installed, the board built with one and the platform configured for the
2001
- other. Reading the version out of the freshly installed \`@meith/web\` keeps
2002
- them the same without anybody having to know the number.
2003
-
2004
- Migrations are forward-only. Recovery is by restore, so take a backup first \u2014
2005
- there is no down migration to undo a destructive one.
1698
+ See [Upgrade Meith](${repositoryUrl}/blob/main/docs/operations/upgrading.md) for validation and recovery.
2006
1699
 
2007
1700
  ## Leaving Vercel
2008
1701
 
2009
- A board must stay movable, and the Blob store is the one part of this shape that
2010
- is not portable: Neon and Upstash hand out ordinary Postgres and Redis strings
2011
- that any host accepts, but a Vercel Blob store is reachable only through Vercel's
2012
- own API and there is no bucket to sync out of it. **The uploads are the thing you
2013
- have to carry out deliberately, and \`meith backup\` is how.**
1702
+ Run backups from a checkout with the correct hosted database credentials and PostgreSQL tools. Set \`FILESTORE_DRIVER=blob\` and a store's \`BLOB_READ_WRITE_TOKEN\`; a local CLI cannot use the deployment's identity. Blob backups include uploads **by default**.
2014
1703
 
2015
- Under \`FILESTORE_DRIVER=blob\`, \`meith backup\` includes the uploads **by
2016
- default** \u2014 it walks the Blob store, pulls every object, and puts them in the
2017
- bundle beside the database dump. This is the opposite of the \`s3\` default, which
2018
- skips them, because a bucket has its own backup story you can drive yourself and
2019
- a Blob store does not:
1704
+ With that environment selected and a writable output directory:
2020
1705
 
2021
1706
  \`\`\`sh
2022
- DATABASE_URL=\u2026 # Neon's pooled string
2023
- DIRECT_DATABASE_URL=\u2026 # Neon's DATABASE_URL_UNPOOLED
2024
- FILESTORE_DRIVER=blob
2025
- BLOB_READ_WRITE_TOKEN=\u2026 # create one on the store; see below
2026
- npm run meith -- backup
2027
- \`\`\`
2028
-
2029
- Run that from a checkout of this repository, with those four values in the
2030
- environment \u2014 the CLI talks to Neon and to the Blob store over the network, so
2031
- it does not have to run on Vercel.
2032
-
2033
- That last one is the one value this route asks you to make by hand, and only
2034
- here. On the deployment the board reaches the store with \`BLOB_STORE_ID\` and
2035
- the deployment's OIDC identity, which a command on your own machine does not
2036
- have. Open the store under **Storage**, create a read-write token, and use it
2037
- for the backup; the board itself never needs it. The bundle it writes holds the dump *and*
2038
- every object. Check the last line it prints: if it says *no uploads*, the
2039
- uploads are not in the bundle and restoring it gives a board whose posts have
2040
- broken images.
2041
-
2042
- Restoring puts them wherever the *restoring* board's \`FILESTORE_DRIVER\` points,
2043
- so the same bundle moves the board either onward or away:
2044
-
2045
- \`\`\`sh
2046
- # onto a self-hosted board with a bucket
2047
- FILESTORE_DRIVER=s3 S3_BUCKET=\u2026 RESTORE_DATABASE_URL=\u2026 npm run meith -- restore bundle.tar.gz
2048
-
2049
- # onto a board that keeps uploads on its own disk
2050
- RESTORE_DATABASE_URL=\u2026 npm run meith -- restore bundle.tar.gz --uploads-dir ./uploads
1707
+ npm run meith -- backup --out ./board-backup.tar.gz --uploads include
2051
1708
  \`\`\`
2052
1709
 
2053
- Take one before you need it. A Blob store deleted with the Vercel project takes
2054
- the attachments with it, and there is no second copy anywhere unless you made
2055
- one.
2056
-
2057
- ## Somewhere other than Vercel
2058
-
2059
- Everything above is one deployment shape.
2060
- [docs/getting-started/deployment/docker-compose.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/docker-compose.md) is the
2061
- same board as containers you run yourself, and \`npx create-meith <name>\`
2062
- scaffolds that shape instead \u2014 a Dockerfile, a compose file and a workflow that
2063
- builds the image. [docs/guides/operations/scaling.md](${repositoryUrl}/blob/main/docs/guides/operations/scaling.md)
2064
- explains why the drivers above are what they are, and why an S3-compatible
2065
- bucket is the portable choice for uploads everywhere but here.
1710
+ Check the result and bundle manifest. Preserve the original \`AUTH_SECRET\` separately. Restore into an empty destination and verify attachments and sign-in before switching traffic or deleting the old services. Follow [Move away from Vercel](${repositoryUrl}/blob/main/docs/operations/leaving-vercel.md) for the complete procedure.
2066
1711
  `;
2067
1712
  }
2068
1713
  function nextSteps(name) {
@@ -2367,7 +2012,7 @@ exercised once, each easy to delete.
2367
2012
  npm test
2368
2013
 
2369
2014
  \`src/plugin.tsx\` is the plugin. What a plugin may and may not do is
2370
- documented in the meith repository under \`docs/customization/plugins.md\`;
2015
+ documented in the meith repository under \`docs/extensions/plugins.md\`;
2371
2016
  every hook and payload is listed in \`docs/reference/plugin-hooks.md\`.
2372
2017
 
2373
2018
  ## Run it inside a board
@@ -2381,8 +2026,7 @@ Scaffold a board next to this directory if you do not have one
2381
2026
  npm installs a local directory as a symlink, so edits here are picked up by
2382
2027
  the board's next build without reinstalling.
2383
2028
 
2384
- Register the plugin in the board's \`meith.plugins.ts\` \u2014 the comment at
2385
- the top of that file shows the shape:
2029
+ Register the plugin in the board's \`meith.plugins.ts\`, keeping its existing entries:
2386
2030
 
2387
2031
  import { messages as ${camel}Messages, plugin as ${camel}Plugin } from '${name}'
2388
2032
 
@@ -2390,12 +2034,12 @@ the top of that file shows the shape:
2390
2034
  { key: '${name}', enabled: true, plugin: ${camel}Plugin, messages: ${camel}Messages },
2391
2035
  ]
2392
2036
 
2393
- and add the matching entry to \`board.plugins.json\`:
2037
+ and add the matching entry to \`board.plugins.json\`, keeping its existing plugins:
2394
2038
 
2395
2039
  { "plugins": [{ "key": "${name}", "package": "${name}", "enabled": true }] }
2396
2040
 
2397
- Rebuild the board (\`npm run build\`) and, because this plugin ships a
2398
- migration, run \`npx meith migrate\`. The plugin then appears under
2041
+ Run \`npm run meith -- upgrade\` against your development database to apply
2042
+ the plugin migration, then rebuild with \`npm run build\`. The plugin appears under
2399
2043
  **Admin \u2192 Plugins**.
2400
2044
 
2401
2045
  ## Publish and list it
@@ -2405,7 +2049,7 @@ migration, run \`npx meith migrate\`. The plugin then appears under
2405
2049
  finish \`listing.json\` (its \`repository\` field starts as a placeholder),
2406
2050
  add the screenshot it names, and open a pull request against the meith
2407
2051
  repository \u2014 the submission process and the review bar are documented there
2408
- in \`docs/customization/marketplace.md\`.
2052
+ in \`docs/extensions/marketplace.md\`.
2409
2053
  `;
2410
2054
  }
2411
2055
  function themeReadme(options, camel, title) {
@@ -2423,7 +2067,7 @@ tokens) plus a single slot override, the footer.
2423
2067
 
2424
2068
  \`src/theme.ts\` declares the theme, \`src/tokens.ts\` carries the palette,
2425
2069
  and slots live in \`src/slots/\`. What a theme may and may not do is
2426
- documented in the meith repository under \`docs/customization/themes.md\`;
2070
+ documented in the meith repository under \`docs/extensions/themes.md\`;
2427
2071
  every slot and view model is listed in \`docs/reference/theme-slots.md\`.
2428
2072
 
2429
2073
  ## Run it inside a board
@@ -2464,7 +2108,7 @@ on the appearance screen and to administrators under **Admin \u2192 Themes**.
2464
2108
  finish \`listing.json\` (its \`repository\` field starts as a placeholder),
2465
2109
  add the screenshot it names, and open a pull request against the meith
2466
2110
  repository \u2014 the submission process and the review bar are documented there
2467
- in \`docs/customization/marketplace.md\`.
2111
+ in \`docs/extensions/marketplace.md\`.
2468
2112
  `;
2469
2113
  }
2470
2114
  function scaffoldPlugin(options) {
@@ -3034,7 +2678,7 @@ async function run(argv, version, updateOptions = {}) {
3034
2678
  }
3035
2679
 
3036
2680
  // src/bin.ts
3037
- var result = await run(process.argv.slice(2), "0.37.3");
2681
+ var result = await run(process.argv.slice(2), "0.37.4");
3038
2682
  for (const line of result.lines) {
3039
2683
  if (result.code === 0) console.log(line);
3040
2684
  else console.error(line);