create-meith 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/scaffold.ts CHANGED
@@ -29,7 +29,7 @@ export const MATERIALIZED_AT_ROOT = [
29
29
  'next-env.d.ts',
30
30
  ]
31
31
 
32
- export const VERCEL_BUILD_COMMAND = `community migrate && forum-web build ${AT_ROOT_FLAG}`
32
+ export const VERCEL_BUILD_COMMAND = `meith migrate && forum-web build ${AT_ROOT_FLAG}`
33
33
 
34
34
  export const TICK_PATH = '/api/system/tick'
35
35
 
@@ -103,7 +103,7 @@ const ENV_DATABASE_URL_PROSE = `# Your Postgres connection string.
103
103
  # processes in front of it, does not need one.`
104
104
 
105
105
  const ENV_DIRECT_DATABASE_URL_PROSE = `# The other half of that pair: the DIRECT (non-pooler) string, used only by
106
- # \`community migrate\` and \`community backup\`. Migrations hold a session-level
106
+ # \`meith migrate\` and \`meith backup\`. Migrations hold a session-level
107
107
  # advisory lock so that two deploys landing together queue instead of both
108
108
  # applying the same migration, and a transaction-mode pooler cannot hold that
109
109
  # lock: it takes the connection back the moment the lock statement ends, which
@@ -238,7 +238,7 @@ DATABASE_URL=
238
238
  ${ENV_DIRECT_DATABASE_URL_PROSE}
239
239
  #
240
240
  # On Vercel this is not optional, and it is no longer yours to copy. DATABASE_URL
241
- # here is the pooler string, the build runs \`community migrate\` against it, and
241
+ # here is the pooler string, the build runs \`meith migrate\` against it, and
242
242
  # /install takes the second of those two session locks on first run. Left blank,
243
243
  # the board reads Neon's own direct string — \`DATABASE_URL_UNPOOLED\` first, then
244
244
  # \`POSTGRES_URL_NON_POOLING\` — and refuses to boot if neither is there, naming
@@ -275,7 +275,7 @@ CRON_SECRET=
275
275
  #
276
276
  # BLOB_READ_WRITE_TOKEN is the other way in, and you make it yourself on the
277
277
  # store. Set it when something has to reach the store from OUTSIDE a Vercel
278
- # deployment — \`community backup\` run on your own machine is the case that
278
+ # deployment — \`meith backup\` run on your own machine is the case that
279
279
  # matters — because there is no OIDC identity there to borrow. Set both and the
280
280
  # board prefers the store id, unless the token names a different store, in which
281
281
  # case the token wins: naming another store is a deliberate act.
@@ -353,7 +353,9 @@ const SELF_HOST_DEPLOY_KIT = [
353
353
  '.github/dependabot.yml',
354
354
  '.github/workflows/build.yml',
355
355
  'Dockerfile',
356
+ 'Dockerfile.prebuilt',
356
357
  'docker-compose.yaml',
358
+ 'docker-compose.prebuilt.yaml',
357
359
  'docker-entrypoint.sh',
358
360
  'docker-healthcheck.sh',
359
361
  ] as const
@@ -433,7 +435,7 @@ export function scaffold(options: ScaffoldOptions): ReadonlyMap<string, string>
433
435
  dev: `forum-web dev${atRootFlag}`,
434
436
  build: `forum-web build${atRootFlag}`,
435
437
  start: `forum-web start${atRootFlag}`,
436
- community: 'community',
438
+ meith: 'meith',
437
439
  },
438
440
  dependencies: {
439
441
  '@meith/web': version,
@@ -523,7 +525,7 @@ export default defineForumConfig({
523
525
  * ]
524
526
  *
525
527
  * and the matching entry in board.plugins.json, which is what
526
- * \`community plugin:add\`/\`plugin:remove\` read inside the monorepo — kept
528
+ * \`meith plugin:add\`/\`plugin:remove\` read inside the monorepo — kept
527
529
  * here too so the two files agree about what is installed.
528
530
  */
529
531
  import type { InstalledPlugin } from '@meith/web/config'
@@ -575,7 +577,88 @@ updates:
575
577
  'Dockerfile',
576
578
  `# syntax=docker/dockerfile:1.7-labs
577
579
  # check=skip=InvalidDefaultArgInFrom
578
- # ${name}'s deploy image.
580
+ # ${name}'s quick-start deploy image — built from source, with nothing to
581
+ # set up first.
582
+ #
583
+ # FROM node:26-alpine directly rather than a published Meith base image:
584
+ # Coolify (or a plain \`docker build\`) builds this from this repository, so
585
+ # there is no registry account, no image tag to paste anywhere, and no
586
+ # \`.github/workflows/build.yml\` run to wait on. The cost of that zero setup
587
+ # is that this installs the board's full dependency closure itself (see the
588
+ # \`npm install\` below), so a build here is heavier than \`Dockerfile.prebuilt\`'s
589
+ # thin delta — that image, pulled rather than built, is the trade the advanced
590
+ # path takes for a low-spec build server or a faster deploy (see \`README.md\`
591
+ # and, in the meith repository, docs/getting-started/deployment/docker-compose.md,
592
+ # "Custom boards").
593
+ #
594
+ # Two stages, not three: unlike the official image, this does not prune down
595
+ # to Next's own standalone output. The migrate role below runs \`meith
596
+ # migrate\`, and \`meith\` materializes @meith/cli's sources and runs them
597
+ # with tsx at the moment it runs (see the meith repository's
598
+ # docs/contributing/development.md, "Consuming the board from a workspace") — it needs
599
+ # the full, un-pruned node_modules tree this board installed, not what Next
600
+ # traced as reachable from the web server alone. The tick itself is driven
601
+ # by docker-compose.yaml's own \`worker\` service — a lightweight loop against
602
+ # /api/system/tick, not a compiled worker process, because @meith/worker is
603
+ # not published (see the meith repository's docs/contributing/release.md).
604
+ FROM node:26-alpine@sha256:aadf416b2cdce311a8811ba3f0608a61b77dbf997500e2eafe781b51f6a0b019 AS deps
605
+ WORKDIR /board
606
+
607
+ # This board's own manifest, cached independently of its source — editing
608
+ # meith.config.ts should not re-run npm install. Nothing warms node_modules
609
+ # ahead of this the way \`Dockerfile.prebuilt\`'s base image does: the full
610
+ # @meith/web, @meith/cli and @meith/theme-default closure this board depends
611
+ # on is installed here, from scratch, which is the heavier half of the
612
+ # quick-start trade.
613
+ COPY package.json ./
614
+ RUN npm install
615
+
616
+ FROM deps AS runtime
617
+ WORKDIR /board
618
+ COPY . .
619
+
620
+ ENV NEXT_TELEMETRY_DISABLED=1
621
+ ENV NODE_ENV=production
622
+
623
+ # DATA_SOURCE is scoped to this one RUN, not declared with ENV — an ENV
624
+ # persists into every container started from this image afterward, and this
625
+ # Dockerfile has no later stage to reset it in (see "Two stages, not three"
626
+ # above). The build needs neither a database nor a production secret (see
627
+ # the meith repository's docs/contributing/development.md, "Fixture mode"), but baking
628
+ # DATA_SOURCE=fixture into the image itself would silently force fixture
629
+ # mode — and with it the in-memory queue driver — at runtime too, no matter
630
+ # what DATABASE_URL an operator supplies to \`docker run\`.
631
+ RUN DATA_SOURCE=fixture npx forum-web build
632
+
633
+ ENV PORT=3000
634
+ ENV HOSTNAME=0.0.0.0
635
+ EXPOSE 3000
636
+
637
+ # node:alpine already carries a non-root "node" user; the board's own files
638
+ # are copied in as root above, so they need handing over before this drops
639
+ # privilege.
640
+ RUN chown -R node:node /board
641
+ USER node
642
+
643
+ COPY --chown=node:node docker-entrypoint.sh docker-healthcheck.sh ./
644
+ RUN chmod +x docker-entrypoint.sh docker-healthcheck.sh
645
+
646
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \\
647
+ CMD ["./docker-healthcheck.sh"]
648
+
649
+ ENTRYPOINT ["./docker-entrypoint.sh"]
650
+ `,
651
+ )
652
+
653
+ files.set(
654
+ 'Dockerfile.prebuilt',
655
+ `# syntax=docker/dockerfile:1.7-labs
656
+ # check=skip=InvalidDefaultArgInFrom
657
+ # ${name}'s advanced deploy image — built by \`.github/workflows/build.yml\` and
658
+ # pulled by \`docker-compose.prebuilt.yaml\`. A quick-start board never builds
659
+ # this file directly; it can delete this file, \`docker-compose.prebuilt.yaml\`
660
+ # and \`.github/workflows/build.yml\` outright and keep only \`Dockerfile\` and
661
+ # \`docker-compose.yaml\` (see README.md, "Deploy").
579
662
  #
580
663
  # FROM the published framework base image — deps + framework layers only,
581
664
  # locked to this exact release (see the meith repository's
@@ -586,8 +669,8 @@ updates:
586
669
  # of minutes rather than a cold toolchain build.
587
670
  #
588
671
  # Two stages, not three: unlike the official image, this does not prune down
589
- # to Next's own standalone output. The migrate role below runs \`community
590
- # migrate\`, and \`community\` materializes @meith/cli's sources and runs them
672
+ # to Next's own standalone output. The migrate role below runs \`meith
673
+ # migrate\`, and \`meith\` materializes @meith/cli's sources and runs them
591
674
  # with tsx at the moment it runs (see the meith repository's
592
675
  # docs/contributing/development.md, "Consuming the board from a workspace") — it needs
593
676
  # the full, un-pruned node_modules tree this board installed, not what Next
@@ -658,22 +741,22 @@ ENTRYPOINT ["./docker-entrypoint.sh"]
658
741
  set -e
659
742
 
660
743
  # An explicit command wins over the role, the same as the official image —
661
- # \`docker run <image> node_modules/.bin/community --help\` should still run
744
+ # \`docker run <image> node_modules/.bin/meith --help\` should still run
662
745
  # the CLI rather than silently starting the web server.
663
746
  if [ "$#" -gt 0 ]; then
664
747
  exec "$@"
665
748
  fi
666
749
 
667
- case "\${COMMUNITY_ROLE:-web}" in
750
+ case "\${MEITH_ROLE:-web}" in
668
751
  migrate)
669
752
  # Runs to completion and exits; compose's one-shot service waits on it.
670
- exec node_modules/.bin/community migrate
753
+ exec node_modules/.bin/meith migrate
671
754
  ;;
672
755
  web)
673
756
  exec node_modules/.bin/forum-web start
674
757
  ;;
675
758
  *)
676
- echo "Unknown COMMUNITY_ROLE: \${COMMUNITY_ROLE}. Expected 'web' or 'migrate'." >&2
759
+ echo "Unknown MEITH_ROLE: \${MEITH_ROLE}. Expected 'web' or 'migrate'." >&2
677
760
  exit 1
678
761
  ;;
679
762
  esac
@@ -688,7 +771,7 @@ esac
688
771
  # a health probe taken while it runs has no opinion.
689
772
  set -e
690
773
 
691
- if [ "\${COMMUNITY_ROLE:-web}" = "migrate" ]; then
774
+ if [ "\${MEITH_ROLE:-web}" = "migrate" ]; then
692
775
  exit 0
693
776
  fi
694
777
 
@@ -710,10 +793,10 @@ node -e "fetch('http://127.0.0.1:3000/api/ready').then(r=>process.exit(r.ok?0:1)
710
793
 
711
794
  files.set(
712
795
  '.github/workflows/build.yml',
713
- `# Builds this board's image and pushes it to your own GHCR, on every push to
714
- # main. No secret to configure: GITHUB_TOKEN is provided automatically by
715
- # GitHub Actions and is enough to push to ghcr.io/<this repository>. See
716
- # README.md for the rest of the three-step deploy story.
796
+ `# The advanced/prebuilt path (see README.md, "Deploy"). Builds this board's
797
+ # Dockerfile.prebuilt and pushes it to your own GHCR, on every push to main.
798
+ # No secret to configure: GITHUB_TOKEN is provided automatically by GitHub
799
+ # Actions and is enough to push to ghcr.io/<this repository>.
717
800
  name: Build and push
718
801
 
719
802
  on:
@@ -748,7 +831,7 @@ jobs:
748
831
  echo "::error::@meith/web in package.json is '$MEITH_VERSION', not an exact X.Y.Z version — that is not a legal Docker image tag. Upgrade with \\\`npm install --save-exact\\\` (see README.md, Upgrading) so this dependency always resolves to one."
749
832
  exit 1
750
833
  fi
751
- docker build --build-arg MEITH_VERSION="$MEITH_VERSION" -t "$IMAGE:\${{ github.sha }}" -t "$IMAGE:latest" .
834
+ docker build -f Dockerfile.prebuilt --build-arg MEITH_VERSION="$MEITH_VERSION" -t "$IMAGE:\${{ github.sha }}" -t "$IMAGE:latest" .
752
835
  docker push "$IMAGE:\${{ github.sha }}"
753
836
  docker push "$IMAGE:latest"
754
837
 
@@ -789,9 +872,142 @@ jobs:
789
872
 
790
873
  files.set(
791
874
  'docker-compose.yaml',
792
- `# ${name}, deployed by Coolify — the same shape as the meith repository's own
793
- # docker/compose.coolify.yml: db, migrate, web, worker. See README.md for
794
- # the three-step deploy story this file is the last step of.
875
+ `# ${name}, quick-start deployed by Coolify — the same shape as the meith
876
+ # repository's own docker/compose.coolify.yml: db, migrate, web, worker. See
877
+ # README.md for the deploy story this file is the last step of.
878
+ #
879
+ # Coolify builds \`Dockerfile\` from this repository itself, so there is no
880
+ # MEITH_IMAGE to set here — every deploy is a build from source. For a
881
+ # low-spec build server or a faster deploy, use \`docker-compose.prebuilt.yaml\`
882
+ # instead, which pulls the image \`.github/workflows/build.yml\` pushes to GHCR.
883
+ #
884
+ # No published ports — Coolify's proxy routes to the container and issues
885
+ # the certificate. The two secrets and the database password are Coolify's
886
+ # own "magic variables": it fills them in on the first deploy and shows them
887
+ # in the panel, so nothing here needs a value typed into it. Requires Coolify
888
+ # v4.0.0-beta.411 or newer, which is when magic variables in a compose file
889
+ # from a Git source arrived.
890
+ services:
891
+ postgres:
892
+ image: postgres:18-alpine@sha256:d3e1620b530c944afa6e887d22eb899824da68e19c52024bf98f5220c88a65b2
893
+ restart: unless-stopped
894
+ mem_limit: \${POSTGRES_MEM_LIMIT:-1g}
895
+ cpus: \${POSTGRES_CPUS:-1}
896
+ environment:
897
+ POSTGRES_USER: community
898
+ POSTGRES_PASSWORD: $SERVICE_PASSWORD_POSTGRES
899
+ POSTGRES_DB: community
900
+ volumes:
901
+ - pgdata:/var/lib/postgresql
902
+ healthcheck:
903
+ test: ['CMD-SHELL', 'pg_isready -U community -d community']
904
+ interval: 10s
905
+ timeout: 5s
906
+ retries: 5
907
+
908
+ # Runs to completion, then exits. web waits for it, so the schema is
909
+ # always applied before the first request rather than racing it.
910
+ migrate:
911
+ build: .
912
+ image: ${name}
913
+ environment:
914
+ MEITH_ROLE: migrate
915
+ DATABASE_URL: postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
916
+ AUTH_SECRET: $SERVICE_BASE64_64_AUTH
917
+ TICK_SECRET: $SERVICE_BASE64_64_TICK
918
+ depends_on:
919
+ postgres:
920
+ condition: service_healthy
921
+ restart: 'no'
922
+
923
+ web:
924
+ build: .
925
+ image: ${name}
926
+ restart: unless-stopped
927
+ mem_limit: \${WEB_MEM_LIMIT:-1g}
928
+ cpus: \${WEB_CPUS:-2}
929
+ # A readiness probe Coolify gates a rolling deploy on: with "Rolling
930
+ # update" enabled on the resource, the new container must answer
931
+ # /api/ready before the old one is retired, so a redeploy swaps in with no
932
+ # gap. Without it Coolify recreates the stack — old removed, then new
933
+ # started — and the board is down while the new web boots.
934
+ healthcheck:
935
+ test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
936
+ interval: 30s
937
+ timeout: 5s
938
+ start_period: 20s
939
+ retries: 3
940
+ environment:
941
+ # Ask Coolify for a domain on port 3000, then hand the board the same
942
+ # thing with a scheme in front.
943
+ - SERVICE_FQDN_WEB_3000
944
+ - APP_URL=$SERVICE_URL_WEB
945
+ - DATABASE_URL=postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
946
+ - AUTH_SECRET=$SERVICE_BASE64_64_AUTH
947
+ - TICK_SECRET=$SERVICE_BASE64_64_TICK
948
+ - QUEUE_DRIVER=postgres
949
+ - CACHE_DRIVER=next
950
+ - FILESTORE_DRIVER=local
951
+ # Left unset, mail is configured on the board itself — the installer
952
+ # asks on first run. Set MAIL_DRIVER here and this file wins instead.
953
+ - MAIL_DRIVER=\${MAIL_DRIVER:-log}
954
+ - MAIL_SMTP_HOST=\${MAIL_SMTP_HOST:-}
955
+ - MAIL_SMTP_PORT=\${MAIL_SMTP_PORT:-}
956
+ - MAIL_SMTP_SECURITY=\${MAIL_SMTP_SECURITY:-}
957
+ - MAIL_SMTP_USERNAME=\${MAIL_SMTP_USERNAME:-}
958
+ - MAIL_SMTP_PASSWORD=\${MAIL_SMTP_PASSWORD:-}
959
+ - MAIL_FROM=\${MAIL_FROM:-}
960
+ volumes:
961
+ - uploads:/app/.uploads
962
+ depends_on:
963
+ postgres:
964
+ condition: service_healthy
965
+ migrate:
966
+ condition: service_completed_successfully
967
+
968
+ # @meith/worker is not published (see the meith repository's
969
+ # docs/contributing/release.md), so there is no compiled worker binary a scaffolded
970
+ # board can run — this drives the tick the alternative way the meith
971
+ # repository documents in docs/getting-started/deployment/docker-compose.md, "Running the tick without
972
+ # a second set of credentials": a small loop calling /api/system/tick.
973
+ worker:
974
+ image: alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
975
+ restart: unless-stopped
976
+ mem_limit: \${WORKER_MEM_LIMIT:-64m}
977
+ cpus: \${WORKER_CPUS:-0.25}
978
+ environment:
979
+ TICK_SECRET: $SERVICE_BASE64_64_TICK
980
+ command:
981
+ - sh
982
+ - -c
983
+ - |
984
+ apk add --no-cache curl >/dev/null
985
+ while true; do
986
+ curl -fsS -m 55 -H "Authorization: Bearer $$TICK_SECRET" \\
987
+ http://web:3000/api/system/tick >/dev/null 2>&1 \\
988
+ || echo "tick failed at $$(date -Is)"
989
+ sleep 60
990
+ done
991
+ depends_on:
992
+ - web
993
+
994
+ volumes:
995
+ pgdata:
996
+ uploads:
997
+ `,
998
+ )
999
+
1000
+ files.set(
1001
+ 'docker-compose.prebuilt.yaml',
1002
+ `# ${name}, deployed by Coolify from a prebuilt image — the advanced path: point
1003
+ # Coolify's compose-file at this file instead of docker-compose.yaml once
1004
+ # \`.github/workflows/build.yml\` has pushed an image, and set MEITH_IMAGE to
1005
+ # what its Summary printed. This trades the quick-start's heavier
1006
+ # build-on-every-deploy for a low-spec build server or a faster deploy — see
1007
+ # README.md, "Deploy".
1008
+ #
1009
+ # Same shape as the meith repository's own docker/compose.coolify.yml: db,
1010
+ # migrate, web, worker.
795
1011
  #
796
1012
  # No published ports — Coolify's proxy routes to the container and issues
797
1013
  # the certificate. The two secrets and the database password are Coolify's
@@ -823,8 +1039,12 @@ services:
823
1039
  # always applied before the first request rather than racing it.
824
1040
  migrate:
825
1041
  image: \${MEITH_IMAGE:?set this to the image the build workflow's Summary just printed, e.g. ghcr.io/<you>/${name}:latest}
1042
+ # Pull the tag on every deploy. Compose keeps an image it already has, so
1043
+ # a rebuilt \`:latest\` is otherwise never fetched and a redeploy quietly
1044
+ # runs the old code.
1045
+ pull_policy: always
826
1046
  environment:
827
- COMMUNITY_ROLE: migrate
1047
+ MEITH_ROLE: migrate
828
1048
  DATABASE_URL: postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
829
1049
  AUTH_SECRET: $SERVICE_BASE64_64_AUTH
830
1050
  TICK_SECRET: $SERVICE_BASE64_64_TICK
@@ -835,9 +1055,21 @@ services:
835
1055
 
836
1056
  web:
837
1057
  image: \${MEITH_IMAGE:?set this to the image the build workflow's Summary just printed, e.g. ghcr.io/<you>/${name}:latest}
1058
+ pull_policy: always
838
1059
  restart: unless-stopped
839
1060
  mem_limit: \${WEB_MEM_LIMIT:-1g}
840
1061
  cpus: \${WEB_CPUS:-2}
1062
+ # A readiness probe Coolify gates a rolling deploy on: with "Rolling
1063
+ # update" enabled on the resource, the new container must answer
1064
+ # /api/ready before the old one is retired, so a redeploy swaps in with no
1065
+ # gap. Without it Coolify recreates the stack — old removed, then new
1066
+ # started — and the board is down while the new web boots.
1067
+ healthcheck:
1068
+ test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
1069
+ interval: 30s
1070
+ timeout: 5s
1071
+ start_period: 20s
1072
+ retries: 3
841
1073
  environment:
842
1074
  # Ask Coolify for a domain on port 3000, then hand the board the same
843
1075
  # thing with a scheme in front.
@@ -906,15 +1138,55 @@ A forum, built on [Meith](${repositoryUrl}).
906
1138
 
907
1139
  ## Deploy
908
1140
 
909
- Nothing here builds on your own server a 2 GB VPS OOMs on a Next.js build,
910
- which is the whole reason \`Dockerfile\`, \`docker-compose.yaml\` and
911
- \`.github/workflows/build.yml\` exist: something else builds the image, the
912
- server only ever pulls one. Three steps, nothing to configure by hand beyond
913
- one value only you know:
1141
+ Two paths onto [Coolify](https://coolify.io), both ending at the same
1142
+ \`/install\`. **Quick start** is the default and needs nothing but a push;
1143
+ **advanced/prebuilt** moves the build off the server, onto GitHub Actions, for
1144
+ a low-spec build server or a faster deploy. Pick one a board only ever runs
1145
+ one of them at a time.
1146
+
1147
+ ### Quick start (default)
1148
+
1149
+ Coolify builds the image itself, from this repository, every time it
1150
+ deploys — there is nothing to push anywhere first and no image tag to paste
1151
+ in. Two steps:
1152
+
1153
+ 1. **Push this repository to GitHub.**
1154
+
1155
+ 2. **Point Coolify at \`docker-compose.yaml\`** — a **Public Git repository**
1156
+ resource with **Docker Compose** as its build pack, this repository as its
1157
+ source. The name is Coolify's own default, so its **Compose file** field is
1158
+ already right when the form opens, and the file already carries Coolify's
1159
+ own "magic variables" for \`AUTH_SECRET\`, \`TICK_SECRET\` and the database
1160
+ password, generated on the first deploy and never typed in. Nothing else to
1161
+ set: \`docker-compose.yaml\` builds \`web\` and \`migrate\` from \`Dockerfile\`
1162
+ itself, so there is no \`MEITH_IMAGE\` here at all.
1163
+
1164
+ 3. **Deploy, then \`/install\` on your own domain.** Coolify issues the
1165
+ certificate; the installer from there is the one
1166
+ [docs/getting-started/deployment/coolify.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/coolify.md#4-run-the-installer)
1167
+ walks through, screen for screen. It seals itself when it finishes, and
1168
+ \`/install\` answers 404 from then on — run it **against the database you
1169
+ are going to keep**. Every push to \`main\` after this is picked up the next
1170
+ time Coolify's own **Redeploy** button runs — pushing alone does not
1171
+ rebuild it.
1172
+
1173
+ The trade for that zero setup is a heavier build: \`Dockerfile\` installs this
1174
+ board's full dependency closure on the server itself, on every deploy, rather
1175
+ than starting from a warm base image. A 2 GB VPS can OOM on it. If that is
1176
+ your server, use the advanced path below instead.
1177
+
1178
+ A quick-start board never needs \`Dockerfile.prebuilt\`,
1179
+ \`docker-compose.prebuilt.yaml\` or \`.github/workflows/build.yml\` — delete all
1180
+ three.
1181
+
1182
+ ### Advanced / prebuilt — for a low-spec server or a faster deploy
1183
+
1184
+ Something else builds the image ahead of time; the server only ever pulls
1185
+ one. Three steps, nothing to configure by hand beyond one value only you know:
914
1186
 
915
1187
  1. **Push this repository to GitHub.** \`.github/workflows/build.yml\` builds
916
- \`Dockerfile\` on every push to \`main\` and pushes the result to your own
917
- GitHub Container Registry, \`ghcr.io/<you>/${name}\` — using only the
1188
+ \`Dockerfile.prebuilt\` on every push to \`main\` and pushes the result to your
1189
+ own GitHub Container Registry, \`ghcr.io/<you>/${name}\` — using only the
918
1190
  \`GITHUB_TOKEN\` every GitHub Actions run already carries. No secret to
919
1191
  add, no registry account beyond the GitHub account you already have.
920
1192
 
@@ -925,29 +1197,26 @@ one value only you know:
925
1197
  repository usually lands public already, and a private one fails
926
1198
  Coolify's pull with an authentication error no operator can act on.
927
1199
 
928
- 2. **Point [Coolify](https://coolify.io) at \`docker-compose.yaml\`** — a
1200
+ 2. **Point Coolify at \`docker-compose.prebuilt.yaml\`** — a
929
1201
  **Public Git repository** resource with **Docker Compose** as its build
930
- pack, this repository as its source. The name is Coolify's own default,
931
- so its **Compose file** field is already right when the form opens, and
932
- the file already carries Coolify's own "magic variables" for
933
- \`AUTH_SECRET\`, \`TICK_SECRET\` and the database password, generated on
934
- the first deploy and never typed in. The one thing Coolify cannot
935
- generate is the image step 1 just pushed: set \`MEITH_IMAGE\` in the
1202
+ pack, this repository as its source, and its **Compose file** field
1203
+ changed from Coolify's default of \`docker-compose.yaml\` to
1204
+ \`docker-compose.prebuilt.yaml\`. That file carries Coolify's own "magic
1205
+ variables" for \`AUTH_SECRET\`, \`TICK_SECRET\` and the database password,
1206
+ generated on the first deploy and never typed in. The one thing Coolify
1207
+ cannot generate is the image step 1 just pushed: set \`MEITH_IMAGE\` in the
936
1208
  resource's own environment to one of the two values that run's Summary
937
- printed (\`docker-compose.yaml\` refuses to start without it, with a
938
- message saying why). \`ghcr.io/<you>/${name}:\${{ github.sha }}\` names
1209
+ printed (\`docker-compose.prebuilt.yaml\` refuses to start without it, with
1210
+ a message saying why). \`ghcr.io/<you>/${name}:\${{ github.sha }}\` names
939
1211
  that one build and nothing else, ever; \`ghcr.io/<you>/${name}:latest\`
940
1212
  follows \`main\` instead, so installing a plugin later is a push and a
941
- **Redeploy** — the trade the quickstart takes, at the cost of an
942
- unrelated redeploy pulling whatever \`main\` most recently built.
1213
+ **Redeploy** — the trade this path takes, at the cost of an unrelated
1214
+ redeploy pulling whatever \`main\` most recently built.
943
1215
 
944
- 3. **Deploy, then \`/install\` on your own domain.** Coolify issues the
945
- certificate; the installer from there is the one
1216
+ 3. **Deploy, then \`/install\` on your own domain.** Same installer, same
946
1217
  [docs/getting-started/deployment/coolify.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/coolify.md#4-run-the-installer)
947
- walks through, screen for screen. It seals itself when it finishes, and
948
- \`/install\` answers 404 from then on run it **against the database you
949
- are going to keep**. Every push to \`main\` after this rebuilds the
950
- image; Coolify's own **Redeploy** button is what actually pulls it —
1218
+ walk-through, same one-time seal. Every push to \`main\` after this rebuilds
1219
+ the image; Coolify's own **Redeploy** button is what actually pulls it
951
1220
  pushing alone does not.
952
1221
 
953
1222
  No Docker Hub, no paid CI: GitHub Actions' free tier and GHCR are the whole
@@ -955,26 +1224,27 @@ build side of this, for a board of any size.
955
1224
 
956
1225
  **Building it yourself**: works on any machine with Docker, if you would
957
1226
  rather not use GitHub Actions for the build — push the result wherever
958
- \`docker-compose.yaml\`'s \`MEITH_IMAGE\` can reach.
1227
+ \`docker-compose.prebuilt.yaml\`'s \`MEITH_IMAGE\` can reach.
959
1228
 
960
1229
  \`\`\`sh
961
- docker build --build-arg MEITH_VERSION=$(node -p "require('./package.json').dependencies['@meith/web']") -t ${name} .
1230
+ docker build -f Dockerfile.prebuilt --build-arg MEITH_VERSION=$(node -p "require('./package.json').dependencies['@meith/web']") -t ${name} .
962
1231
  \`\`\`
963
1232
 
964
1233
  **Without a panel**: [docs/getting-started/deployment/docker-compose.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/docker-compose.md)
965
1234
  is the same four containers by hand — your own \`.env\`, a reverse proxy you
966
1235
  already run, no Coolify. \`Dockerfile\` and \`docker-compose.yaml\` here are this
967
- board's own version of exactly that shape.
1236
+ board's own version of exactly that shape (or \`Dockerfile.prebuilt\` and
1237
+ \`docker-compose.prebuilt.yaml\`, for the advanced path).
968
1238
 
969
- Two things nothing configures for you:
1239
+ Two things nothing configures for you, on either path:
970
1240
 
971
1241
  - **Mail.** Until \`MAIL_DRIVER\` and its three settings exist, every message is
972
1242
  written to the log and delivered to nobody, so password reset fails silently.
973
- - **The tick.** \`docker-compose.yaml\`'s \`worker\` service drives it here — a small
1243
+ - **The tick.** The compose file's \`worker\` service drives it here — a small
974
1244
  loop calling \`/api/system/tick\` once a minute, since \`@meith/web\`'s own
975
1245
  worker package is not something a board outside the meith monorepo can
976
1246
  depend on yet. Deploy some other way and something still has to call that
977
- route (or run \`community task:run\`) every minute, or nothing catches up
1247
+ route (or run \`meith task:run\`) every minute, or nothing catches up
978
1248
  and nothing errors.
979
1249
 
980
1250
  ## Local
@@ -992,8 +1262,8 @@ Posting needs Postgres. Copy \`.env.example\` to \`.env.local\`, set
992
1262
  \`DATABASE_URL\` and the two secrets in it, then:
993
1263
 
994
1264
  \`\`\`sh
995
- npm run community -- migrate
996
- echo "<password>" | npm run community -- user:create --username <name> --email <address> --group administrators
1265
+ npm run meith -- migrate
1266
+ echo "<password>" | npm run meith -- user:create --username <name> --email <address> --group administrators
997
1267
  \`\`\`
998
1268
 
999
1269
  ## Configuring
@@ -1004,9 +1274,51 @@ echo "<password>" | npm run community -- user:create --username <name> --email <
1004
1274
  - **\`/admin\`** — settings, forums, groups, members, themes, maintenance. An
1005
1275
  administrator re-enters their password to get in, and again for anything
1006
1276
  destructive.
1007
- - **\`npm run community -- --help\`** — the operator CLI. Everything the panel does
1277
+ - **\`npm run meith -- --help\`** — the operator CLI. Everything the panel does
1008
1278
  and a few things it cannot, without a browser.
1009
1279
 
1280
+ ## Installing plugins and themes
1281
+
1282
+ Nothing installs into a running container — a plugin or theme has to be
1283
+ built into the image, the same as any other dependency:
1284
+
1285
+ 1. **In this repository**, install it:
1286
+
1287
+ \`\`\`sh
1288
+ npm install --save-exact @meith/plugin-dues
1289
+ \`\`\`
1290
+
1291
+ (a theme is the same command with its own package, e.g.
1292
+ \`@meith/theme-midnight\`).
1293
+
1294
+ 2. **Register it.** A **theme** goes in \`meith.config.ts\`, in the \`themes\`
1295
+ map, following the shape of the \`default\` entry already there. A
1296
+ **plugin** goes in \`meith.plugins.ts\`: import its \`plugin\` and
1297
+ \`messages\` exports and add \`{ key, enabled: true, plugin, messages }\`
1298
+ to \`INSTALLED_PLUGINS\` — or run
1299
+
1300
+ \`\`\`sh
1301
+ npm run meith -- plugin:add @meith/plugin-dues
1302
+ \`\`\`
1303
+
1304
+ which edits \`board.plugins.json\` and regenerates \`meith.plugins.ts\`
1305
+ for you.
1306
+
1307
+ 3. **Commit and push**, then **Redeploy** from Coolify — pushing alone does
1308
+ not rebuild. Quick start builds the new image on that redeploy; advanced/prebuilt
1309
+ waits for \`.github/workflows/build.yml\` to finish first, and Redeploy is
1310
+ what actually pulls the result.
1311
+
1312
+ 4. **Once it is up, run its migrations one time:**
1313
+
1314
+ \`\`\`sh
1315
+ docker compose run --rm web meith upgrade
1316
+ \`\`\`
1317
+
1318
+ See [docs/customization/plugins.md](${repositoryUrl}/blob/main/docs/customization/plugins.md)
1319
+ and [docs/customization/themes.md](${repositoryUrl}/blob/main/docs/customization/themes.md)
1320
+ for the full reference.
1321
+
1010
1322
  ## Upgrading
1011
1323
 
1012
1324
  \`\`\`sh
@@ -1031,19 +1343,23 @@ weekly pull request bumping the actions pinned in
1031
1343
  \`.github/workflows/build.yml\`, which is a safe, independent update the two
1032
1344
  commands above never touch.
1033
1345
 
1034
- That \`package.json\` change is the whole pin: \`Dockerfile\`'s own
1035
- \`FROM\` line takes the version as a build argument, and
1036
- \`.github/workflows/build.yml\` reads it straight out of \`package.json\`'s
1037
- own \`@meith/web\` dependency when it rebuilds nothing in \`Dockerfile\`
1038
- itself to keep in sync by hand. \`--save-exact\` matters: npm's default
1039
- \`save-prefix\` is \`^\`, and a caret range is not a legal Docker image tag —
1040
- without it, this exact command would write \`"^0.18.0"\` and the next build
1041
- would fail with \`invalid reference format\` instead of building. This
1346
+ On the quick-start path there is no version to keep in sync by hand:
1347
+ \`Dockerfile\` runs \`npm install\` straight from this \`package.json\` on every
1348
+ build, so a rebuild always picks up whatever is pinned there. On the
1349
+ advanced/prebuilt path, that \`package.json\` change is the whole pin:
1350
+ \`Dockerfile.prebuilt\`'s own \`FROM\` line takes the version as a build argument,
1351
+ and \`.github/workflows/build.yml\` reads it straight out of \`package.json\`'s
1352
+ own \`@meith/web\` dependency when it rebuilds nothing in
1353
+ \`Dockerfile.prebuilt\` itself to keep in sync by hand. \`--save-exact\` matters
1354
+ either way: npm's default \`save-prefix\` is \`^\`, and a caret range is not a
1355
+ legal Docker image tag for the advanced path — without it, this exact command
1356
+ would write \`"^0.18.0"\` and the next \`Dockerfile.prebuilt\` build would fail
1357
+ with \`invalid reference format\` instead of building. This
1042
1358
  project's own \`.npmrc\` sets \`save-exact=true\` for the same reason, so an
1043
1359
  \`npm install\` of anything else here — a plugin, say — stays pinned too; the
1044
1360
  build workflow also refuses to build from anything but an exact version, as
1045
1361
  a second line of defense. Once the rebuilt image is deployed, run
1046
- \`npm run community -- upgrade\` against it for the plugin migrations — see
1362
+ \`npm run meith -- upgrade\` against it for the plugin migrations — see
1047
1363
  [the operator CLI](${repositoryUrl}/blob/main/docs/guides/operations/operating.md#the-operator-cli)
1048
1364
  for running it against this deployment.
1049
1365
 
@@ -1303,9 +1619,9 @@ A board must stay movable, and the Blob store is the one part of this shape that
1303
1619
  is not portable: Neon and Upstash hand out ordinary Postgres and Redis strings
1304
1620
  that any host accepts, but a Vercel Blob store is reachable only through Vercel's
1305
1621
  own API and there is no bucket to sync out of it. **The uploads are the thing you
1306
- have to carry out deliberately, and \`community backup\` is how.**
1622
+ have to carry out deliberately, and \`meith backup\` is how.**
1307
1623
 
1308
- Under \`FILESTORE_DRIVER=blob\`, \`community backup\` includes the uploads **by
1624
+ Under \`FILESTORE_DRIVER=blob\`, \`meith backup\` includes the uploads **by
1309
1625
  default** — it walks the Blob store, pulls every object, and puts them in the
1310
1626
  bundle beside the database dump. This is the opposite of the \`s3\` default, which
1311
1627
  skips them, because a bucket has its own backup story you can drive yourself and
@@ -1316,7 +1632,7 @@ DATABASE_URL=… # Neon's pooled string
1316
1632
  DIRECT_DATABASE_URL=… # Neon's DATABASE_URL_UNPOOLED
1317
1633
  FILESTORE_DRIVER=blob
1318
1634
  BLOB_READ_WRITE_TOKEN=… # create one on the store; see below
1319
- npm run community -- backup
1635
+ npm run meith -- backup
1320
1636
  \`\`\`
1321
1637
 
1322
1638
  Run that from a checkout of this repository, with those four values in the
@@ -1337,10 +1653,10 @@ so the same bundle moves the board either onward or away:
1337
1653
 
1338
1654
  \`\`\`sh
1339
1655
  # onto a self-hosted board with a bucket
1340
- FILESTORE_DRIVER=s3 S3_BUCKET=… RESTORE_DATABASE_URL=… npm run community -- restore bundle.tar.gz
1656
+ FILESTORE_DRIVER=s3 S3_BUCKET=… RESTORE_DATABASE_URL=… npm run meith -- restore bundle.tar.gz
1341
1657
 
1342
1658
  # onto a board that keeps uploads on its own disk
1343
- RESTORE_DATABASE_URL=… npm run community -- restore bundle.tar.gz --uploads-dir ./uploads
1659
+ RESTORE_DATABASE_URL=… npm run meith -- restore bundle.tar.gz --uploads-dir ./uploads
1344
1660
  \`\`\`
1345
1661
 
1346
1662
  Take one before you need it. A Blob store deleted with the Vercel project takes