create-meith 0.37.2 → 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/src/scaffold.ts CHANGED
@@ -496,7 +496,7 @@ save-exact=true
496
496
  *
497
497
  * Adding a theme is: \`npm install\` it, add a line here, redeploy. Adding a
498
498
  * plugin is the same, through board.plugins.json and meith.plugins.ts —
499
- * see docs/customization/plugins.md.
499
+ * see docs/extensions/plugins.md.
500
500
  */
501
501
  import { defineForumConfig } from '@meith/web/config'
502
502
  import {
@@ -537,7 +537,7 @@ export default defineForumConfig({
537
537
  // them. A plugin that does not fit that convention can be added here by hand instead —
538
538
  // keep it out of board.plugins.json so a regenerate does not drop it.
539
539
  //
540
- // docs/customization/plugins.md explains both.
540
+ // docs/extensions/plugins.md explains both.
541
541
 
542
542
  import type { InstalledPlugin } from '@meith/web/config'
543
543
 
@@ -677,7 +677,7 @@ jobs:
677
677
  # \`npm install\` below), so a build here is heavier than \`Dockerfile.prebuilt\`'s
678
678
  # thin delta — that image, pulled rather than built, is the trade the advanced
679
679
  # path takes for a low-spec build server or a faster deploy (see \`README.md\`
680
- # and, in the meith repository, docs/getting-started/deployment/docker-compose.md,
680
+ # and, in the meith repository, docs/operations/docker-compose.md,
681
681
  # "Custom boards").
682
682
  #
683
683
  # Two stages, not three: unlike the official image, this does not prune down
@@ -776,7 +776,7 @@ ENTRYPOINT ["./docker-entrypoint.sh"]
776
776
  #
777
777
  # FROM the published framework base image — deps + framework layers only,
778
778
  # locked to this exact release (see the meith repository's
779
- # docs/getting-started/deployment/docker-compose.md, "Custom boards", and docker/Dockerfile.base for what
779
+ # docs/operations/docker-compose.md, "Custom boards", and docker/Dockerfile.base for what
780
780
  # it is and is not). This board's own Dockerfile only ever installs its own
781
781
  # delta on top of it — a new plugin's own dependency, typically nothing more
782
782
  # — which is what keeps a rebuild after \`npm install some-plugin\` a matter
@@ -1120,7 +1120,7 @@ services:
1120
1120
  # service the variables the file names, so a Scheduled Task running
1121
1121
  # \`meith backup\` in this container would never see them without these
1122
1122
  # lines — see the meith repository's
1123
- # docs/getting-started/deployment/coolify.md, "Set up backups".
1123
+ # docs/operations/coolify.md, "Set up backups".
1124
1124
  - BACKUP_S3_BUCKET=\${BACKUP_S3_BUCKET:-}
1125
1125
  - BACKUP_S3_REGION=\${BACKUP_S3_REGION:-}
1126
1126
  - BACKUP_S3_ACCESS_KEY_ID=\${BACKUP_S3_ACCESS_KEY_ID:-}
@@ -1146,7 +1146,7 @@ services:
1146
1146
  # @meith/worker is not published (see the meith repository's
1147
1147
  # docs/contributing/release.md), so there is no compiled worker binary a scaffolded
1148
1148
  # board can run — this drives the tick the alternative way the meith
1149
- # repository documents in docs/getting-started/deployment/docker-compose.md, "Running the tick without
1149
+ # repository documents in docs/operations/docker-compose.md, "Running the tick without
1150
1150
  # a second set of credentials": a small loop calling /api/system/tick.
1151
1151
  worker:
1152
1152
  image: alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
@@ -1293,7 +1293,7 @@ services:
1293
1293
  # service the variables the file names, so a Scheduled Task running
1294
1294
  # \`meith backup\` in this container would never see them without these
1295
1295
  # lines — see the meith repository's
1296
- # docs/getting-started/deployment/coolify.md, "Set up backups".
1296
+ # docs/operations/coolify.md, "Set up backups".
1297
1297
  - BACKUP_S3_BUCKET=\${BACKUP_S3_BUCKET:-}
1298
1298
  - BACKUP_S3_REGION=\${BACKUP_S3_REGION:-}
1299
1299
  - BACKUP_S3_ACCESS_KEY_ID=\${BACKUP_S3_ACCESS_KEY_ID:-}
@@ -1319,7 +1319,7 @@ services:
1319
1319
  # @meith/worker is not published (see the meith repository's
1320
1320
  # docs/contributing/release.md), so there is no compiled worker binary a scaffolded
1321
1321
  # board can run — this drives the tick the alternative way the meith
1322
- # repository documents in docs/getting-started/deployment/docker-compose.md, "Running the tick without
1322
+ # repository documents in docs/operations/docker-compose.md, "Running the tick without
1323
1323
  # a second set of credentials": a small loop calling /api/system/tick.
1324
1324
  worker:
1325
1325
  image: alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
@@ -1358,7 +1358,7 @@ volumes:
1358
1358
  # secrets for you: every value Coolify would have filled in — the database
1359
1359
  # password, AUTH_SECRET, TICK_SECRET, the board's own address — comes from
1360
1360
  # a \`.env\` beside this file instead. See the meith repository's
1361
- # docs/getting-started/deployment/docker-compose.md, which this file is
1361
+ # docs/operations/docker-compose.md, which this file is
1362
1362
  # the last step of.
1363
1363
  #
1364
1364
  # \`docker compose\` only auto-discovers a file literally named
@@ -1374,7 +1374,7 @@ volumes:
1374
1374
  # to GitHub and let \`.github/workflows/build.yml\` do it, or run
1375
1375
  # \`docker build -f Dockerfile.prebuilt ...\` by hand) and change the two
1376
1376
  # \`build: .\` lines below to \`image: <that image>:<version>\` — the
1377
- # substitution docs/getting-started/deployment/docker-compose.md, "Building
1377
+ # substitution docs/operations/docker-compose.md, "Building
1378
1378
  # somewhere else", walks through.
1379
1379
  services:
1380
1380
  postgres:
@@ -1456,7 +1456,7 @@ services:
1456
1456
  APP_URL: \${APP_URL:-http://localhost:3000}
1457
1457
  # One reverse proxy (Caddy, in the guide's own walkthrough) sits in
1458
1458
  # front of \`web\` — see "Count your proxies" in
1459
- # docs/getting-started/deployment/docker-compose.md.
1459
+ # docs/operations/docker-compose.md.
1460
1460
  TRUSTED_PROXY_HOPS: \${TRUSTED_PROXY_HOPS:-1}
1461
1461
  # Leaving this at \`log\` does not mean no mail: it means the board
1462
1462
  # decides, from the installer on first run or from
@@ -1519,7 +1519,7 @@ services:
1519
1519
  depends_on:
1520
1520
  - web
1521
1521
 
1522
- # Off by default — see docs/guides/operations/scaling.md before setting
1522
+ # Off by default — see docs/operations/scaling.md before setting
1523
1523
  # CACHE_DRIVER=redis above. This is the server it needs.
1524
1524
  redis:
1525
1525
  profiles: ['redis']
@@ -1549,264 +1549,91 @@ volumes:
1549
1549
  'README.md',
1550
1550
  `# ${name}
1551
1551
 
1552
- A forum, built on [Meith](${repositoryUrl}).
1553
-
1554
- ## Deploy
1555
-
1556
- Three paths onto a server, all ending at the same \`/install\`. **Quick
1557
- start** onto [Coolify](https://coolify.io) is the default and needs nothing
1558
- but a push; **advanced/prebuilt** moves the build off the server, onto
1559
- GitHub Actions, for a low-spec build server or a faster deploy; **without a
1560
- panel** is the same four containers run by hand, with your own \`.env\` and
1561
- reverse proxy, and no Coolify at all. Pick one — a board only ever runs one
1562
- of them at a time.
1563
-
1564
- ### Quick start (default)
1565
-
1566
- Coolify builds the image itself, from this repository, every time it
1567
- deploys — there is nothing to push anywhere first and no image tag to paste
1568
- in. Two steps:
1569
-
1570
- 1. **Push this repository to GitHub.**
1571
-
1572
- 2. **Point Coolify at \`docker-compose.yaml\`** — a **Public Git repository**
1573
- resource with **Docker Compose** as its build pack, this repository as its
1574
- source. The name is Coolify's own default, so its **Compose file** field is
1575
- already right when the form opens, and the file already carries Coolify's
1576
- own "magic variables" for \`AUTH_SECRET\`, \`TICK_SECRET\` and the database
1577
- password, generated on the first deploy and never typed in. Nothing else to
1578
- set: \`docker-compose.yaml\` builds \`web\` and \`migrate\` from \`Dockerfile\`
1579
- itself, so there is no \`MEITH_IMAGE\` here at all.
1580
-
1581
- 3. **Deploy, then \`/install\` on your own domain.** Coolify issues the
1582
- certificate; the installer from there is the one
1583
- [docs/getting-started/deployment/coolify.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/coolify.md#4-run-the-installer)
1584
- walks through, screen for screen. It seals itself when it finishes, and
1585
- \`/install\` answers 404 from then on — run it **against the database you
1586
- are going to keep**. Every push to \`main\` after this is picked up the next
1587
- time Coolify's own **Redeploy** button runs — pushing alone does not
1588
- rebuild it.
1589
-
1590
- The trade for that zero setup is a heavier build: \`Dockerfile\` installs this
1591
- board's full dependency closure on the server itself, on every deploy, rather
1592
- than starting from a warm base image. A 2 GB VPS can OOM on it. If that is
1593
- your server, use the advanced path below instead.
1594
-
1595
- A quick-start board never needs \`Dockerfile.prebuilt\`,
1596
- \`docker-compose.prebuilt.yaml\` or \`.github/workflows/build.yml\` — delete all
1597
- three.
1598
-
1599
- ### Advanced / prebuilt — for a low-spec server or a faster deploy
1600
-
1601
- Something else builds the image ahead of time; the server only ever pulls
1602
- one. Three steps, nothing to configure by hand beyond one value only you know:
1603
-
1604
- 1. **Push this repository to GitHub.** \`.github/workflows/build.yml\` builds
1605
- \`Dockerfile.prebuilt\` on every push to \`main\` and pushes the result to your
1606
- own GitHub Container Registry, \`ghcr.io/<you>/${name}\` — using only the
1607
- \`GITHUB_TOKEN\` every GitHub Actions run already carries. No secret to
1608
- add, no registry account beyond the GitHub account you already have.
1609
-
1610
- That build is the thing step 2 waits on: open the repository's
1611
- **Actions** tab and let the run finish, because its **Summary** is where
1612
- the exact image to paste into step 2 comes from. The Summary also links
1613
- the package itself, to check it is public — a build from a public
1614
- repository usually lands public already, and a private one fails
1615
- Coolify's pull with an authentication error no operator can act on.
1616
-
1617
- 2. **Point Coolify at \`docker-compose.prebuilt.yaml\`** — a
1618
- **Public Git repository** resource with **Docker Compose** as its build
1619
- pack, this repository as its source, and its **Compose file** field
1620
- changed from Coolify's default of \`docker-compose.yaml\` to
1621
- \`docker-compose.prebuilt.yaml\`. That file carries Coolify's own "magic
1622
- variables" for \`AUTH_SECRET\`, \`TICK_SECRET\` and the database password,
1623
- generated on the first deploy and never typed in. The one thing Coolify
1624
- cannot generate is the image step 1 just pushed: set \`MEITH_IMAGE\` in the
1625
- resource's own environment to one of the two values that run's Summary
1626
- printed (\`docker-compose.prebuilt.yaml\` refuses to start without it, with
1627
- a message saying why). \`ghcr.io/<you>/${name}:\${{ github.sha }}\` names
1628
- that one build and nothing else, ever; \`ghcr.io/<you>/${name}:latest\`
1629
- follows \`main\` instead, so installing a plugin later is a push and a
1630
- **Redeploy** — the trade this path takes, at the cost of an unrelated
1631
- redeploy pulling whatever \`main\` most recently built.
1632
-
1633
- 3. **Deploy, then \`/install\` on your own domain.** Same installer, same
1634
- [docs/getting-started/deployment/coolify.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/coolify.md#4-run-the-installer)
1635
- walk-through, same one-time seal. Every push to \`main\` after this rebuilds
1636
- the image; Coolify's own **Redeploy** button is what actually pulls it —
1637
- pushing alone does not.
1638
-
1639
- No Docker Hub, no paid CI: GitHub Actions' free tier and GHCR are the whole
1640
- build side of this, for a board of any size.
1641
-
1642
- **Building it yourself**: works on any machine with Docker, if you would
1643
- rather not use GitHub Actions for the build — push the result wherever
1644
- \`docker-compose.prebuilt.yaml\`'s \`MEITH_IMAGE\` can reach.
1552
+ A community board built on [Meith](${repositoryUrl}).
1645
1553
 
1646
- \`\`\`sh
1647
- docker build -f Dockerfile.prebuilt --build-arg MEITH_VERSION=$(node -p "require('./package.json').dependencies['@meith/web']") -t ${name} .
1648
- \`\`\`
1554
+ ## Choose a deployment
1649
1555
 
1650
- ### Without a panel
1556
+ | Route | File | Where the image builds |
1557
+ |---|---|---|
1558
+ | Quick start with Coolify | \`docker-compose.yaml\` | Your server |
1559
+ | Advanced / prebuilt | \`docker-compose.prebuilt.yaml\` | GitHub Actions or another build machine |
1560
+ | Docker Compose without a panel | \`docker-compose.byhand.yaml\` | Your server |
1651
1561
 
1652
- \`docker-compose.byhand.yaml\`, beside the two Coolify files above, is the same
1653
- four containers deployed with nothing generating secrets for you: a \`.env\`
1654
- you write yourself, a port published for the reverse proxy you already run,
1655
- and \`docker compose up -d --build\` in place of a panel's Deploy button.
1656
- [docs/getting-started/deployment/docker-compose.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/docker-compose.md)
1657
- is the full walkthrough this file is the last step of, including the
1658
- \`.env\` this repository does not carry — nothing here belongs in git. Delete
1659
- this file if you know you will only ever deploy through Coolify; keep it,
1660
- and it needs nothing else changed, if you later want to move away from
1661
- Coolify without changing how the board itself is built.
1562
+ 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.
1662
1563
 
1663
- Two things nothing configures for you, on any path:
1564
+ ### Quick start with Coolify
1664
1565
 
1665
- - **Mail.** Until \`MAIL_DRIVER\` and its three settings exist, every message is
1666
- written to the log and delivered to nobody, so password reset fails silently.
1667
- - **The tick.** The compose file's \`worker\` service drives it here a small
1668
- loop calling \`/api/system/tick\` once a minute, since \`@meith/web\`'s own
1669
- worker package is not something a board outside the meith monorepo can
1670
- depend on yet. Deploy some other way and something still has to call that
1671
- route (or run \`meith task:run\`) every minute, or nothing catches up
1672
- and nothing errors.
1566
+ 1. Push this repository to GitHub.
1567
+ 2. Create a Git repository resource in Coolify with the Docker Compose build pack and \`/docker-compose.yaml\` as the Compose file.
1568
+ 3. Assign the board's domain and deploy. Coolify supplies database and authentication secrets; save a protected recovery copy.
1569
+ 4. Confirm \`postgres\` is healthy, \`migrate\` exits successfully, and \`web\` and \`worker\` run.
1570
+ 5. Open \`/install\`, unlock with \`AUTH_SECRET\`, and create the board and its first administrator. The installer seals itself and returns 404 after completion.
1673
1571
 
1674
- ## Local
1572
+ 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.
1675
1573
 
1676
- \`\`\`sh
1677
- npm install
1678
- npm run dev
1679
- \`\`\`
1574
+ ### Advanced / prebuilt
1680
1575
 
1681
- No environment file, no database: with no \`DATABASE_URL\` the board serves
1682
- deterministic in-memory sample data, which is enough to click through every
1683
- reading surface.
1576
+ 1. Let \`.github/workflows/build.yml\` finish in GitHub Actions. It builds and publishes your board's image.
1577
+ 2. Make that image accessible to Coolify and select \`/docker-compose.prebuilt.yaml\`.
1578
+ 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.
1579
+ 4. Deploy and complete \`/install\` as above.
1684
1580
 
1685
- Posting needs Postgres. Copy \`.env.example\` to \`.env.local\`, set
1686
- \`DATABASE_URL\` and the two secrets in it, then:
1581
+ For a local image build, the build argument comes from the board's pinned package:
1687
1582
 
1688
1583
  \`\`\`sh
1689
- npm run meith -- migrate
1690
- echo "<password>" | npm run meith -- user:create --username <name> --email <address> --group administrators
1584
+ docker build -f Dockerfile.prebuilt --build-arg MEITH_VERSION=$(node -p "require('./package.json').dependencies['@meith/web']") -t ${name} .
1691
1585
  \`\`\`
1692
1586
 
1693
- ## Configuring
1587
+ After changing the board, wait for its new image and update \`MEITH_IMAGE\` if pinned to a commit, then redeploy.
1694
1588
 
1695
- - **\`meith.config.ts\`** — installed themes and plugins. Everything installable
1696
- is named here so the bundler can see it; nothing is found by scanning a
1697
- directory at runtime.
1698
- - **\`/admin\`** — settings, forums, groups, members, themes, maintenance. An
1699
- administrator re-enters their password to get in, and again for anything
1700
- destructive.
1701
- - **\`npm run meith -- --help\`** — the operator CLI. Everything the panel does
1702
- and a few things it cannot, without a browser.
1589
+ ## Run locally
1703
1590
 
1704
- ## Installing plugins and themes
1591
+ \`\`\`sh
1592
+ npm install
1593
+ npm run dev
1594
+ \`\`\`
1705
1595
 
1706
- Nothing installs into a running container a plugin or theme has to be
1707
- built into the image. In this repository:
1596
+ 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).
1708
1597
 
1709
- 1. **Add it.** A **plugin** is one command, which installs the package and
1710
- registers it:
1598
+ ## Configure the community
1711
1599
 
1712
- \`\`\`sh
1713
- npm run meith -- plugin:add @meith/plugin-dues
1714
- \`\`\`
1600
+ - \`meith.config.ts\` registers themes and board configuration.
1601
+ - \`board.plugins.json\` and \`meith.plugins.ts\` register installed plugins.
1602
+ - \`/admin\` manages forums, members, permissions and settings.
1603
+ - \`npm run meith -- --help\` lists operator commands.
1715
1604
 
1716
- It writes \`board.plugins.json\` and regenerates \`meith.plugins.ts\` for you
1717
- (\`npm run meith -- plugin:remove <key>\` reverses it). A **theme** is
1718
- \`npm install --save-exact @meith/theme-midnight\`, then an entry in
1719
- \`meith.config.ts\`'s \`themes\` map following the shape of the \`default\` one
1720
- already there — set \`defaultTheme\` to its key to make it the board's
1721
- default.
1605
+ 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\`.
1722
1606
 
1723
- 2. **Commit and push**, then **Redeploy** from Coolify — pushing alone does
1724
- not rebuild. Quick start builds the new image on that redeploy; advanced/prebuilt
1725
- waits for \`.github/workflows/build.yml\` to finish first, and Redeploy is
1726
- what actually pulls the result.
1607
+ ## Install an extension
1727
1608
 
1728
- 3. **If it ships database changes, apply them once it is up** — from
1729
- **Admin → System** (**Version & migrations**) in the browser, or:
1609
+ Add a plugin from this checkout:
1730
1610
 
1731
- \`\`\`sh
1732
- docker compose run --rm web meith upgrade
1733
- \`\`\`
1611
+ \`\`\`sh
1612
+ npm run meith -- plugin:add @meith/plugin-dues
1613
+ \`\`\`
1734
1614
 
1735
- See [Installing plugins and themes](${repositoryUrl}/blob/main/docs/customization/installing.md)
1736
- for the full guide.
1615
+ 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.
1737
1616
 
1738
1617
  ## Upgrading
1739
1618
 
1740
- \`.github/workflows/update.yml\` does this for you: once a week and
1741
- whenever you press **Run workflow** on the Actions tab — it checks for a new
1742
- Meith release and opens a pull request that moves every \`@meith/*\` package
1743
- and \`next\` together, and rewrites the deploy files this scaffold owns
1744
- (\`Dockerfile\`, the compose files, the workflows) to the new release's
1745
- shape. A file you have edited yourself is never rewritten; the run's log
1746
- names any it left for you. One-time setup: under
1747
- **Settings → Actions → General**, enable **Allow GitHub Actions to create
1748
- and approve pull requests**, or the workflow cannot open one.
1619
+ \`.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 → Actions → General**.
1749
1620
 
1750
- Merging that pull request is still an upgrade, not a formality: read the
1751
- release notes it links, take a backup first, and press **Redeploy** in
1752
- Coolify after the merge — pushing alone does not rebuild. Once the new
1753
- version serves, run \`npm run meith -- upgrade\` against it for the plugin
1754
- migrations.
1621
+ 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.
1755
1622
 
1756
- The same update, by hand and without waiting for the schedule:
1623
+ To prepare the update locally:
1757
1624
 
1758
1625
  \`\`\`sh
1759
1626
  npx create-meith@latest update
1760
- git commit -am "Update Meith"
1761
- git push
1762
1627
  \`\`\`
1763
1628
 
1764
- Under the hood, the version move is these two commands, plus the deploy-file
1765
- rewrite neither of them can do:
1629
+ The updater moves package pins and supported deployment files together. Its package update includes these commands; running them alone does not update deployment files:
1766
1630
 
1767
1631
  \`\`\`sh
1768
1632
  npm install --save-exact @meith/web@latest @meith/cli@latest @meith/theme-default@latest
1769
1633
  npm install --save-exact next@$(node -p "require('./node_modules/@meith/web/package.json').dependencies.next")
1770
1634
  \`\`\`
1771
1635
 
1772
- The second command is not optional. This board pins \`next\` itself, and
1773
- the npm commands alone never bump it: upgrading only the \`@meith/*\` packages
1774
- leaves the board's own pin on the old Next while \`@meith/web\` depends on the
1775
- new one, which npm resolves by installing both — the build then runs on one
1776
- version while everything reading \`package.json\` sees the other. Reading the
1777
- version out of the freshly installed \`@meith/web\` is what keeps the two the
1778
- same without anybody having to know the number.
1779
-
1780
- \`next\` and \`@meith/web\` move together or not at all, which is why one
1781
- updater owns the whole move and no dependency bot bumps either on its own.
1782
- What Dependabot *does* keep current is this repository's own GitHub Actions —
1783
- \`.github/dependabot.yml\` opens a weekly pull request bumping the actions
1784
- pinned under \`.github/workflows\`, a safe, independent update the updater
1785
- leaves to it.
1786
-
1787
- On the quick-start path there is no version to keep in sync by hand:
1788
- \`Dockerfile\` runs \`npm install\` straight from this \`package.json\` on every
1789
- build, so a rebuild always picks up whatever is pinned there. On the
1790
- advanced/prebuilt path, that \`package.json\` change is the whole pin:
1791
- \`Dockerfile.prebuilt\`'s own \`FROM\` line takes the version as a build argument,
1792
- and \`.github/workflows/build.yml\` reads it straight out of \`package.json\`'s
1793
- own \`@meith/web\` dependency when it rebuilds — nothing in
1794
- \`Dockerfile.prebuilt\` itself to keep in sync by hand. \`--save-exact\` matters
1795
- either way: npm's default \`save-prefix\` is \`^\`, and a caret range is not a
1796
- legal Docker image tag for the advanced path — without it, this exact command
1797
- would write \`"^0.18.0"\` and the next \`Dockerfile.prebuilt\` build would fail
1798
- with \`invalid reference format\` instead of building. This
1799
- project's own \`.npmrc\` sets \`save-exact=true\` for the same reason, so an
1800
- \`npm install\` of anything else here — a plugin, say — stays pinned too; the
1801
- build workflow also refuses to build from anything but an exact version, as
1802
- a second line of defense. Once the rebuilt image is deployed, run
1803
- \`npm run meith -- upgrade\` against it for the plugin migrations — see
1804
- [the operator CLI](${repositoryUrl}/blob/main/docs/guides/operations/operating.md#the-operator-cli)
1805
- for running it against this deployment.
1806
-
1807
- Migrations are forward-only. Recovery is by restore, so take a backup first —
1808
- there is no down migration to undo a destructive one, and a button that pretended
1809
- otherwise would be worse than its absence.
1636
+ 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.
1810
1637
  `,
1811
1638
  )
1812
1639
 
@@ -1860,277 +1687,95 @@ ${AT_ROOT_IGNORES}
1860
1687
  function vercelReadme({ name, repositoryUrl, templateRepositoryUrl }: VercelTreeOptions): string {
1861
1688
  return `# ${name}
1862
1689
 
1863
- A forum, built on [Meith](${repositoryUrl}), running as Vercel functions.
1690
+ A community board built on [Meith](${repositoryUrl}), deployed as Vercel functions.
1864
1691
 
1865
1692
  [![Deploy with Vercel](https://vercel.com/button)](${deployButtonUrl(templateRepositoryUrl)})
1866
1693
 
1867
- ## What the button provisions
1868
-
1869
- - **A copy of this repository** under your own GitHub account. Vercel builds
1870
- from it, and every later push to \`main\` redeploys.
1871
- - **A Neon Postgres database**, attached to the project. Neon publishes the
1872
- pooled connection string as \`DATABASE_URL\` and the direct one as
1873
- \`DATABASE_URL_UNPOOLED\`.
1874
- - **An Upstash Redis store**, attached the same way, for the shared cache. It
1875
- publishes \`KV_URL\`, which the board reads as \`REDIS_URL\` — \`KV_REST_API_URL\`
1876
- beside it is an HTTPS endpoint and is not used for this.
1877
- - **A Vercel Blob store** for uploads, which publishes \`BLOB_STORE_ID\` into the
1878
- project by itself. That is the whole credential: the board hands the id to
1879
- Vercel's SDK, which authenticates with the deployment's own OIDC identity, so
1880
- there is no token to copy. This is what used to be four hand-typed \`S3_*\`
1881
- secrets.
1882
- - **A Resend mail account**, attached the same way, which publishes
1883
- \`RESEND_API_KEY\` and \`RESEND_EMAIL_DOMAIN\`. The board reads both names
1884
- directly: its mail driver already speaks Resend's request shape, so there is
1885
- nothing to adapt, and the sending domain is what the sender is built from.
1886
- - **A Vercel project** carrying \`vercel.json\` — the build command
1887
- \`${VERCEL_BUILD_COMMAND}\`,
1888
- which applies the schema before it builds, materializes the board's app at
1889
- the project root so the artefact lands where Vercel reads it, and the cron
1890
- entry that drives the tick.
1891
-
1892
- **Mail needs no variables after the deploy.** Resend publishes both its key
1893
- and its sending domain, and the board sends from \`${RESEND_SENDER_MAILBOX}@\`
1894
- that domain — see *Mail* below to send from a different address, and for the
1895
- one case that does need you: a domain Resend has not verified yet.
1896
-
1897
- ## What to type into the deploy form
1898
-
1899
- **Two secrets**, generated rather than chosen. Thirty-two characters is a floor
1900
- the board enforces at boot, not a suggestion:
1694
+ ## 1. Connect services and set secrets
1695
+
1696
+ 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.
1697
+
1698
+ Generate two independent secrets:
1901
1699
 
1902
1700
  \`\`\`sh
1903
- openssl rand -hex 32 # AUTH_SECRET
1904
- openssl rand -hex 32 # CRON_SECRET
1701
+ openssl rand -hex 32
1702
+ openssl rand -hex 32
1905
1703
  \`\`\`
1906
1704
 
1907
- \`CRON_SECRET\` is the name Vercel Cron sends, as \`Authorization: Bearer\`, and it
1908
- cannot be told to send another — the caller is the platform, so this one has to
1909
- be an environment variable both ends can read, and cannot be something the
1910
- board makes up for itself. Note that this floor is stricter than the 16
1911
- characters Vercel's own cron documentation suggests — a value generated by
1912
- following those instructions is refused here, and the fix is a longer secret.
1705
+ 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.
1913
1706
 
1914
- \`AUTH_SECRET\` seals members' two-factor secrets and signs the unsubscribe links
1915
- in outgoing mail. It stays in the environment deliberately: a copy of the
1916
- database is then not enough to forge either.
1917
-
1918
- **That is the whole form.** Everything else the board works out from the stores
1919
- this button just linked to the project:
1707
+ The platform derives the following defaults when you have not supplied explicit overrides:
1920
1708
 
1921
1709
  \`\`\`ini
1922
1710
  ${VERCEL_DERIVED_DRIVERS.join('\n')}
1923
1711
  \`\`\`
1924
1712
 
1925
- \`DIRECT_DATABASE_URL\` comes from Neon's own \`DATABASE_URL_UNPOOLED\`, or
1926
- \`POSTGRES_URL_NON_POOLING\` if that one is absent — migrations and the first-run
1927
- installer each hold a session-level advisory lock, which the pooled
1928
- \`DATABASE_URL\` cannot hold. \`REDIS_URL\` comes from Upstash's \`KV_URL\`, the one
1929
- variable it publishes that speaks the Redis protocol.
1930
-
1931
- Every one of those derivations is scoped to this platform, fires only where you
1932
- have not set the variable yourself, and **refuses to boot rather than guess**.
1933
- If a store is missing, or publishes a name this board does not know, the deploy
1934
- stops with a message naming every variable it looked at — it will not fall back
1935
- to caching inside each instance, or to uploads on a disk that is discarded with
1936
- the instance. When the name is one we do not know, set \`REDIS_URL\` or
1937
- \`DIRECT_DATABASE_URL\` in the project's environment settings and the derivation
1938
- stands aside.
1939
-
1940
- If you would rather keep uploads somewhere you hold yourself — see *Leaving
1941
- Vercel* below for why that matters — set \`FILESTORE_DRIVER=s3\` and add
1942
- \`S3_BUCKET\`, \`S3_REGION\`, \`S3_ACCESS_KEY_ID\` and \`S3_SECRET_ACCESS_KEY\` in the
1943
- project's environment settings, with \`S3_ENDPOINT\` for a bucket that is not AWS
1944
- (\`S3_REGION=auto\` for R2). The same board runs either way.
1713
+ | Service value | Meith uses it for |
1714
+ |---|---|
1715
+ | \`DATABASE_URL\` | Runtime database connection |
1716
+ | \`DATABASE_URL_UNPOOLED\`, falling back to \`POSTGRES_URL_NON_POOLING\` | \`DIRECT_DATABASE_URL\` for migrations and installer locks |
1717
+ | \`KV_URL\` | \`REDIS_URL\`; the Redis protocol connection, not the HTTP REST endpoint |
1718
+ | \`BLOB_STORE_ID\` | Upload storage authenticated through the deployment identity |
1719
+ | \`RESEND_API_KEY\`, \`RESEND_EMAIL_DOMAIN\` | HTTP mail credentials and sender |
1720
+
1721
+ 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.
1722
+
1723
+ ## 2. Deploy and install
1724
+
1725
+ 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.
1726
+
1727
+ 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.
1945
1728
 
1946
1729
  ## Mail
1947
1730
 
1948
- **There is nothing to set.** The Resend the deploy form added publishes two
1949
- names into the project: \`RESEND_API_KEY\`, and \`RESEND_EMAIL_DOMAIN\` — the
1950
- domain it sends from. The board reads both, sends from
1951
- \`${RESEND_SENDER_MAILBOX}@\` that domain, and posts over Resend's HTTPS API.
1952
-
1953
- **If Resend refuses the messages**, that domain is not verified yet. Resend
1954
- will not send from a domain it has not verified, whoever set the address, so
1955
- verify it from the Resend dashboard — the deploy cannot do that step for you,
1956
- because it is Resend confirming you own the domain. This is the one thing here
1957
- that can need attention, and it announces itself: the test button below says
1958
- so rather than the board failing quietly.
1959
-
1960
- **To send from a different address**, set \`MAIL_FROM\` in the project's
1961
- environment settings and redeploy. It must be at a domain Resend has verified,
1962
- for the same reason. An address you set always wins over the derived one.
1963
-
1964
- A board with the key but no verified domain — which is what you get if you
1965
- remove the integration's \`RESEND_EMAIL_DOMAIN\` without putting a
1966
- \`MAIL_FROM\` in its place — does not guess a sender. It stays on the log
1967
- driver and delivers nothing, which is the honest outcome: a guessed sender at
1968
- an unverified domain would be refused by Resend anyway, one message at a
1969
- time.
1970
-
1971
- The board is not tied to Resend. Its mail driver is a plain JSON-over-HTTPS
1972
- sender that posts \`{from, to, subject, text, html, reply_to}\` with a bearer
1973
- token — Resend's \`POST /emails\` happens to be exactly that shape, which is why
1974
- it needs no adapter. Any provider with the same shape works: set
1975
- \`MAIL_HTTP_ENDPOINT\`, \`MAIL_HTTP_TOKEN\` and \`MAIL_DRIVER=http\` in the
1976
- project's environment settings, and set the first two **together** — either
1977
- one on its own stands the Resend bridge down, so a key issued for Resend is
1978
- never presented to an endpoint you chose. Setting \`MAIL_DRIVER\` to anything
1979
- but \`http\` stands the bridge down too, for the same reason: a board that
1980
- moved to SMTP must not send through its new provider from Resend's domain.
1981
- Delete \`RESEND_API_KEY\` once you have moved off Resend.
1982
-
1983
- Check it worked: sign in as the administrator and use the test button on
1984
- **/admin → Settings → Mail**.
1985
-
1986
- ## First run: \`/install\`
1987
-
1988
- The build applies migrations, but an empty schema is not yet a board. Open
1989
- \`https://<your-deployment>/install\` once the first deploy is green. It asks for
1990
- the board's name and address and for the first administrator's username, email
1991
- and password, creates the board and that account, and then **seals itself**:
1992
- \`/install\` answers 404 from then on. Run it against the database you intend to
1993
- keep — the screens are the ones
1994
- [docs/getting-started/deployment/docker-compose.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/docker-compose.md#6-install-it)
1995
- walks through.
1996
-
1997
- ## The tick
1998
-
1999
- \`vercel.json\` asks Vercel to call \`${TICK_PATH}\` on \`${TICK_SCHEDULE}\`. That
2000
- route is how bans expire, digests send, mail leaves the outbox and the queue
2001
- drains; nothing here runs it on its own, because there is no worker process on
2002
- a function platform. Two things about it are worth knowing **before** you
2003
- deploy rather than after:
2004
-
2005
- - **This ships a daily schedule, because Hobby refuses anything faster.** A
2006
- Hobby plan rejects a cron expression that would run more than once a day —
2007
- the deployment fails outright rather than being slowed down — so
2008
- \`vercel.json\` carries \`${TICK_SCHEDULE}\` and deploys anywhere. On a
2009
- paid plan, edit it to \`* * * * *\` and the board ticks every minute.
2010
-
2011
- A daily tick loses nothing permanently: tasks are written so a missed run
2012
- delays work rather than dropping it, and a password reset is sent as the
2013
- request is handled rather than waiting for a tick. What it does delay is
2014
- everything the tick drives — a new post is not findable in search, and a
2015
- notification is not sent, until the next run.
2016
-
2017
- **To keep a fast tick without paying**, drive \`${TICK_PATH}\` from anything
2018
- that can call a URL on a schedule — a GitHub Actions workflow, a systemd
2019
- timer, an uptime pinger — presenting \`TICK_SECRET\` instead of
2020
- \`CRON_SECRET\`. The endpoint accepts either, so the Vercel cron and an
2021
- outside scheduler can both drive it.
2022
- - **\`maxDuration = 300\` is validated when the project builds, not when the
2023
- function runs.** A plan that does not allow 300 seconds therefore **fails the
2024
- deployment** rather than clamping the request. With Fluid Compute — the
2025
- default for new projects — Hobby allows 300 and this builds as written. With
2026
- Fluid Compute switched off, Hobby caps a function at 60 seconds and the build
2027
- fails. Turn Fluid Compute back on.
2028
-
2029
- A tick that reaches the tasks and runs them answers \`200\` even when one of them
2030
- threw, with \`ok: false\` and the failure named in \`ran\`. That is deliberate:
2031
- schedulers retry non-2xx answers, and a task that fails every time would turn
2032
- each retry into another attempt against whatever it is failing against.
1731
+ 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 → Settings → Mail**.
1732
+
1733
+ 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.
1734
+
1735
+ 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).
1736
+
1737
+ ## 3. Verify scheduled work and hosting limits
1738
+
1739
+ \`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.
1740
+
1741
+ 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).
1742
+
1743
+ 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.
1744
+
1745
+ 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.
2033
1746
 
2034
1747
  ## Upgrading
2035
1748
 
2036
- \`.github/workflows/update.yml\` does this for you: once a week and
2037
- whenever you press **Run workflow** on the Actions tab — it checks for a new
2038
- Meith release and opens a pull request that moves every \`@meith/*\` package
2039
- and \`next\` together. A file you have edited yourself is never rewritten; the
2040
- run's log names any it left for you. One-time setup: under
2041
- **Settings → Actions → General**, enable **Allow GitHub Actions to create and
2042
- approve pull requests**, or the workflow cannot open one. Read the release
2043
- notes the pull request links and take a backup before merging; Vercel
2044
- rebuilds on the merge, and the build command applies the new migrations
2045
- before it builds.
1749
+ \`.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 → Actions → General**.
1750
+
1751
+ 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.
2046
1752
 
2047
- The same update, by hand and without waiting for the schedule:
1753
+ To prepare the update locally:
2048
1754
 
2049
1755
  \`\`\`sh
2050
1756
  npx create-meith@latest update
2051
- git commit -am "Update Meith"
2052
- git push
2053
1757
  \`\`\`
2054
1758
 
2055
- Under the hood, the version move is these two commands:
1759
+ The updater also reconciles supported deployment files. Its package update keeps Meith and Next.js aligned:
2056
1760
 
2057
1761
  \`\`\`sh
2058
1762
  npm install --save-exact @meith/web@latest @meith/cli@latest @meith/theme-default@latest
2059
1763
  npm install --save-exact next@$(node -p "require('./node_modules/@meith/web/package.json').dependencies.next")
2060
1764
  \`\`\`
2061
1765
 
2062
- \`--save-exact\` matters and \`.npmrc\` already sets it for everything else
2063
- installed here.
2064
-
2065
- The second command is not optional. This board pins \`next\` itself — Vercel
2066
- reads that pin to pick its Next.js builder — and the npm commands alone never
2067
- bump it. Upgrading only the \`@meith/*\` packages leaves two versions of Next
2068
- installed, the board built with one and the platform configured for the
2069
- other. Reading the version out of the freshly installed \`@meith/web\` keeps
2070
- them the same without anybody having to know the number.
2071
-
2072
- Migrations are forward-only. Recovery is by restore, so take a backup first —
2073
- there is no down migration to undo a destructive one.
1766
+ See [Upgrade Meith](${repositoryUrl}/blob/main/docs/operations/upgrading.md) for validation and recovery.
2074
1767
 
2075
1768
  ## Leaving Vercel
2076
1769
 
2077
- A board must stay movable, and the Blob store is the one part of this shape that
2078
- is not portable: Neon and Upstash hand out ordinary Postgres and Redis strings
2079
- that any host accepts, but a Vercel Blob store is reachable only through Vercel's
2080
- own API and there is no bucket to sync out of it. **The uploads are the thing you
2081
- have to carry out deliberately, and \`meith backup\` is how.**
1770
+ 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**.
2082
1771
 
2083
- Under \`FILESTORE_DRIVER=blob\`, \`meith backup\` includes the uploads **by
2084
- default** — it walks the Blob store, pulls every object, and puts them in the
2085
- bundle beside the database dump. This is the opposite of the \`s3\` default, which
2086
- skips them, because a bucket has its own backup story you can drive yourself and
2087
- a Blob store does not:
1772
+ With that environment selected and a writable output directory:
2088
1773
 
2089
1774
  \`\`\`sh
2090
- DATABASE_URL=… # Neon's pooled string
2091
- DIRECT_DATABASE_URL=… # Neon's DATABASE_URL_UNPOOLED
2092
- FILESTORE_DRIVER=blob
2093
- BLOB_READ_WRITE_TOKEN=… # create one on the store; see below
2094
- npm run meith -- backup
2095
- \`\`\`
2096
-
2097
- Run that from a checkout of this repository, with those four values in the
2098
- environment — the CLI talks to Neon and to the Blob store over the network, so
2099
- it does not have to run on Vercel.
2100
-
2101
- That last one is the one value this route asks you to make by hand, and only
2102
- here. On the deployment the board reaches the store with \`BLOB_STORE_ID\` and
2103
- the deployment's OIDC identity, which a command on your own machine does not
2104
- have. Open the store under **Storage**, create a read-write token, and use it
2105
- for the backup; the board itself never needs it. The bundle it writes holds the dump *and*
2106
- every object. Check the last line it prints: if it says *no uploads*, the
2107
- uploads are not in the bundle and restoring it gives a board whose posts have
2108
- broken images.
2109
-
2110
- Restoring puts them wherever the *restoring* board's \`FILESTORE_DRIVER\` points,
2111
- so the same bundle moves the board either onward or away:
2112
-
2113
- \`\`\`sh
2114
- # onto a self-hosted board with a bucket
2115
- FILESTORE_DRIVER=s3 S3_BUCKET=… RESTORE_DATABASE_URL=… npm run meith -- restore bundle.tar.gz
2116
-
2117
- # onto a board that keeps uploads on its own disk
2118
- RESTORE_DATABASE_URL=… npm run meith -- restore bundle.tar.gz --uploads-dir ./uploads
1775
+ npm run meith -- backup --out ./board-backup.tar.gz --uploads include
2119
1776
  \`\`\`
2120
1777
 
2121
- Take one before you need it. A Blob store deleted with the Vercel project takes
2122
- the attachments with it, and there is no second copy anywhere unless you made
2123
- one.
2124
-
2125
- ## Somewhere other than Vercel
2126
-
2127
- Everything above is one deployment shape.
2128
- [docs/getting-started/deployment/docker-compose.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/docker-compose.md) is the
2129
- same board as containers you run yourself, and \`npx create-meith <name>\`
2130
- scaffolds that shape instead — a Dockerfile, a compose file and a workflow that
2131
- builds the image. [docs/guides/operations/scaling.md](${repositoryUrl}/blob/main/docs/guides/operations/scaling.md)
2132
- explains why the drivers above are what they are, and why an S3-compatible
2133
- bucket is the portable choice for uploads everywhere but here.
1778
+ 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.
2134
1779
  `
2135
1780
  }
2136
1781