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/dist/bin.mjs CHANGED
@@ -23,7 +23,7 @@ var MATERIALIZED_AT_ROOT = [
23
23
  "tsconfig.json",
24
24
  "next-env.d.ts"
25
25
  ];
26
- var VERCEL_BUILD_COMMAND = `community migrate && forum-web build ${AT_ROOT_FLAG}`;
26
+ var VERCEL_BUILD_COMMAND = `meith migrate && forum-web build ${AT_ROOT_FLAG}`;
27
27
  var TICK_PATH = "/api/system/tick";
28
28
  var TICK_SCHEDULE = "0 3 * * *";
29
29
  var RESEND_SENDER_MAILBOX = "noreply";
@@ -85,7 +85,7 @@ var ENV_DATABASE_URL_PROSE = `# Your Postgres connection string.
85
85
  # database rather than the cause. Your own Postgres, with a fixed number of
86
86
  # processes in front of it, does not need one.`;
87
87
  var ENV_DIRECT_DATABASE_URL_PROSE = `# The other half of that pair: the DIRECT (non-pooler) string, used only by
88
- # \`community migrate\` and \`community backup\`. Migrations hold a session-level
88
+ # \`meith migrate\` and \`meith backup\`. Migrations hold a session-level
89
89
  # advisory lock so that two deploys landing together queue instead of both
90
90
  # applying the same migration, and a transaction-mode pooler cannot hold that
91
91
  # lock: it takes the connection back the moment the lock statement ends, which
@@ -213,7 +213,7 @@ DATABASE_URL=
213
213
  ${ENV_DIRECT_DATABASE_URL_PROSE}
214
214
  #
215
215
  # On Vercel this is not optional, and it is no longer yours to copy. DATABASE_URL
216
- # here is the pooler string, the build runs \`community migrate\` against it, and
216
+ # here is the pooler string, the build runs \`meith migrate\` against it, and
217
217
  # /install takes the second of those two session locks on first run. Left blank,
218
218
  # the board reads Neon's own direct string \u2014 \`DATABASE_URL_UNPOOLED\` first, then
219
219
  # \`POSTGRES_URL_NON_POOLING\` \u2014 and refuses to boot if neither is there, naming
@@ -250,7 +250,7 @@ CRON_SECRET=
250
250
  #
251
251
  # BLOB_READ_WRITE_TOKEN is the other way in, and you make it yourself on the
252
252
  # store. Set it when something has to reach the store from OUTSIDE a Vercel
253
- # deployment \u2014 \`community backup\` run on your own machine is the case that
253
+ # deployment \u2014 \`meith backup\` run on your own machine is the case that
254
254
  # matters \u2014 because there is no OIDC identity there to borrow. Set both and the
255
255
  # board prefers the store id, unless the token names a different store, in which
256
256
  # case the token wins: naming another store is a deliberate act.
@@ -326,7 +326,9 @@ var SELF_HOST_DEPLOY_KIT = [
326
326
  ".github/dependabot.yml",
327
327
  ".github/workflows/build.yml",
328
328
  "Dockerfile",
329
+ "Dockerfile.prebuilt",
329
330
  "docker-compose.yaml",
331
+ "docker-compose.prebuilt.yaml",
330
332
  "docker-entrypoint.sh",
331
333
  "docker-healthcheck.sh"
332
334
  ];
@@ -399,7 +401,7 @@ function scaffold(options) {
399
401
  dev: `forum-web dev${atRootFlag}`,
400
402
  build: `forum-web build${atRootFlag}`,
401
403
  start: `forum-web start${atRootFlag}`,
402
- community: "community"
404
+ meith: "meith"
403
405
  },
404
406
  dependencies: {
405
407
  "@meith/web": version,
@@ -487,7 +489,7 @@ export default defineForumConfig({
487
489
  * ]
488
490
  *
489
491
  * and the matching entry in board.plugins.json, which is what
490
- * \`community plugin:add\`/\`plugin:remove\` read inside the monorepo \u2014 kept
492
+ * \`meith plugin:add\`/\`plugin:remove\` read inside the monorepo \u2014 kept
491
493
  * here too so the two files agree about what is installed.
492
494
  */
493
495
  import type { InstalledPlugin } from '@meith/web/config'
@@ -535,7 +537,87 @@ updates:
535
537
  "Dockerfile",
536
538
  `# syntax=docker/dockerfile:1.7-labs
537
539
  # check=skip=InvalidDefaultArgInFrom
538
- # ${name}'s deploy image.
540
+ # ${name}'s quick-start deploy image \u2014 built from source, with nothing to
541
+ # set up first.
542
+ #
543
+ # FROM node:26-alpine directly rather than a published Meith base image:
544
+ # Coolify (or a plain \`docker build\`) builds this from this repository, so
545
+ # there is no registry account, no image tag to paste anywhere, and no
546
+ # \`.github/workflows/build.yml\` run to wait on. The cost of that zero setup
547
+ # is that this installs the board's full dependency closure itself (see the
548
+ # \`npm install\` below), so a build here is heavier than \`Dockerfile.prebuilt\`'s
549
+ # thin delta \u2014 that image, pulled rather than built, is the trade the advanced
550
+ # path takes for a low-spec build server or a faster deploy (see \`README.md\`
551
+ # and, in the meith repository, docs/getting-started/deployment/docker-compose.md,
552
+ # "Custom boards").
553
+ #
554
+ # Two stages, not three: unlike the official image, this does not prune down
555
+ # to Next's own standalone output. The migrate role below runs \`meith
556
+ # migrate\`, and \`meith\` materializes @meith/cli's sources and runs them
557
+ # with tsx at the moment it runs (see the meith repository's
558
+ # docs/contributing/development.md, "Consuming the board from a workspace") \u2014 it needs
559
+ # the full, un-pruned node_modules tree this board installed, not what Next
560
+ # traced as reachable from the web server alone. The tick itself is driven
561
+ # by docker-compose.yaml's own \`worker\` service \u2014 a lightweight loop against
562
+ # /api/system/tick, not a compiled worker process, because @meith/worker is
563
+ # not published (see the meith repository's docs/contributing/release.md).
564
+ FROM node:26-alpine@sha256:aadf416b2cdce311a8811ba3f0608a61b77dbf997500e2eafe781b51f6a0b019 AS deps
565
+ WORKDIR /board
566
+
567
+ # This board's own manifest, cached independently of its source \u2014 editing
568
+ # meith.config.ts should not re-run npm install. Nothing warms node_modules
569
+ # ahead of this the way \`Dockerfile.prebuilt\`'s base image does: the full
570
+ # @meith/web, @meith/cli and @meith/theme-default closure this board depends
571
+ # on is installed here, from scratch, which is the heavier half of the
572
+ # quick-start trade.
573
+ COPY package.json ./
574
+ RUN npm install
575
+
576
+ FROM deps AS runtime
577
+ WORKDIR /board
578
+ COPY . .
579
+
580
+ ENV NEXT_TELEMETRY_DISABLED=1
581
+ ENV NODE_ENV=production
582
+
583
+ # DATA_SOURCE is scoped to this one RUN, not declared with ENV \u2014 an ENV
584
+ # persists into every container started from this image afterward, and this
585
+ # Dockerfile has no later stage to reset it in (see "Two stages, not three"
586
+ # above). The build needs neither a database nor a production secret (see
587
+ # the meith repository's docs/contributing/development.md, "Fixture mode"), but baking
588
+ # DATA_SOURCE=fixture into the image itself would silently force fixture
589
+ # mode \u2014 and with it the in-memory queue driver \u2014 at runtime too, no matter
590
+ # what DATABASE_URL an operator supplies to \`docker run\`.
591
+ RUN DATA_SOURCE=fixture npx forum-web build
592
+
593
+ ENV PORT=3000
594
+ ENV HOSTNAME=0.0.0.0
595
+ EXPOSE 3000
596
+
597
+ # node:alpine already carries a non-root "node" user; the board's own files
598
+ # are copied in as root above, so they need handing over before this drops
599
+ # privilege.
600
+ RUN chown -R node:node /board
601
+ USER node
602
+
603
+ COPY --chown=node:node docker-entrypoint.sh docker-healthcheck.sh ./
604
+ RUN chmod +x docker-entrypoint.sh docker-healthcheck.sh
605
+
606
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \\
607
+ CMD ["./docker-healthcheck.sh"]
608
+
609
+ ENTRYPOINT ["./docker-entrypoint.sh"]
610
+ `
611
+ );
612
+ files.set(
613
+ "Dockerfile.prebuilt",
614
+ `# syntax=docker/dockerfile:1.7-labs
615
+ # check=skip=InvalidDefaultArgInFrom
616
+ # ${name}'s advanced deploy image \u2014 built by \`.github/workflows/build.yml\` and
617
+ # pulled by \`docker-compose.prebuilt.yaml\`. A quick-start board never builds
618
+ # this file directly; it can delete this file, \`docker-compose.prebuilt.yaml\`
619
+ # and \`.github/workflows/build.yml\` outright and keep only \`Dockerfile\` and
620
+ # \`docker-compose.yaml\` (see README.md, "Deploy").
539
621
  #
540
622
  # FROM the published framework base image \u2014 deps + framework layers only,
541
623
  # locked to this exact release (see the meith repository's
@@ -546,8 +628,8 @@ updates:
546
628
  # of minutes rather than a cold toolchain build.
547
629
  #
548
630
  # Two stages, not three: unlike the official image, this does not prune down
549
- # to Next's own standalone output. The migrate role below runs \`community
550
- # migrate\`, and \`community\` materializes @meith/cli's sources and runs them
631
+ # to Next's own standalone output. The migrate role below runs \`meith
632
+ # migrate\`, and \`meith\` materializes @meith/cli's sources and runs them
551
633
  # with tsx at the moment it runs (see the meith repository's
552
634
  # docs/contributing/development.md, "Consuming the board from a workspace") \u2014 it needs
553
635
  # the full, un-pruned node_modules tree this board installed, not what Next
@@ -617,22 +699,22 @@ ENTRYPOINT ["./docker-entrypoint.sh"]
617
699
  set -e
618
700
 
619
701
  # An explicit command wins over the role, the same as the official image \u2014
620
- # \`docker run <image> node_modules/.bin/community --help\` should still run
702
+ # \`docker run <image> node_modules/.bin/meith --help\` should still run
621
703
  # the CLI rather than silently starting the web server.
622
704
  if [ "$#" -gt 0 ]; then
623
705
  exec "$@"
624
706
  fi
625
707
 
626
- case "\${COMMUNITY_ROLE:-web}" in
708
+ case "\${MEITH_ROLE:-web}" in
627
709
  migrate)
628
710
  # Runs to completion and exits; compose's one-shot service waits on it.
629
- exec node_modules/.bin/community migrate
711
+ exec node_modules/.bin/meith migrate
630
712
  ;;
631
713
  web)
632
714
  exec node_modules/.bin/forum-web start
633
715
  ;;
634
716
  *)
635
- echo "Unknown COMMUNITY_ROLE: \${COMMUNITY_ROLE}. Expected 'web' or 'migrate'." >&2
717
+ echo "Unknown MEITH_ROLE: \${MEITH_ROLE}. Expected 'web' or 'migrate'." >&2
636
718
  exit 1
637
719
  ;;
638
720
  esac
@@ -646,7 +728,7 @@ esac
646
728
  # a health probe taken while it runs has no opinion.
647
729
  set -e
648
730
 
649
- if [ "\${COMMUNITY_ROLE:-web}" = "migrate" ]; then
731
+ if [ "\${MEITH_ROLE:-web}" = "migrate" ]; then
650
732
  exit 0
651
733
  fi
652
734
 
@@ -666,10 +748,10 @@ node -e "fetch('http://127.0.0.1:3000/api/ready').then(r=>process.exit(r.ok?0:1)
666
748
  );
667
749
  files.set(
668
750
  ".github/workflows/build.yml",
669
- `# Builds this board's image and pushes it to your own GHCR, on every push to
670
- # main. No secret to configure: GITHUB_TOKEN is provided automatically by
671
- # GitHub Actions and is enough to push to ghcr.io/<this repository>. See
672
- # README.md for the rest of the three-step deploy story.
751
+ `# The advanced/prebuilt path (see README.md, "Deploy"). Builds this board's
752
+ # Dockerfile.prebuilt and pushes it to your own GHCR, on every push to main.
753
+ # No secret to configure: GITHUB_TOKEN is provided automatically by GitHub
754
+ # Actions and is enough to push to ghcr.io/<this repository>.
673
755
  name: Build and push
674
756
 
675
757
  on:
@@ -704,7 +786,7 @@ jobs:
704
786
  echo "::error::@meith/web in package.json is '$MEITH_VERSION', not an exact X.Y.Z version \u2014 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."
705
787
  exit 1
706
788
  fi
707
- docker build --build-arg MEITH_VERSION="$MEITH_VERSION" -t "$IMAGE:\${{ github.sha }}" -t "$IMAGE:latest" .
789
+ docker build -f Dockerfile.prebuilt --build-arg MEITH_VERSION="$MEITH_VERSION" -t "$IMAGE:\${{ github.sha }}" -t "$IMAGE:latest" .
708
790
  docker push "$IMAGE:\${{ github.sha }}"
709
791
  docker push "$IMAGE:latest"
710
792
 
@@ -744,9 +826,141 @@ jobs:
744
826
  );
745
827
  files.set(
746
828
  "docker-compose.yaml",
747
- `# ${name}, deployed by Coolify \u2014 the same shape as the meith repository's own
748
- # docker/compose.coolify.yml: db, migrate, web, worker. See README.md for
749
- # the three-step deploy story this file is the last step of.
829
+ `# ${name}, quick-start deployed by Coolify \u2014 the same shape as the meith
830
+ # repository's own docker/compose.coolify.yml: db, migrate, web, worker. See
831
+ # README.md for the deploy story this file is the last step of.
832
+ #
833
+ # Coolify builds \`Dockerfile\` from this repository itself, so there is no
834
+ # MEITH_IMAGE to set here \u2014 every deploy is a build from source. For a
835
+ # low-spec build server or a faster deploy, use \`docker-compose.prebuilt.yaml\`
836
+ # instead, which pulls the image \`.github/workflows/build.yml\` pushes to GHCR.
837
+ #
838
+ # No published ports \u2014 Coolify's proxy routes to the container and issues
839
+ # the certificate. The two secrets and the database password are Coolify's
840
+ # own "magic variables": it fills them in on the first deploy and shows them
841
+ # in the panel, so nothing here needs a value typed into it. Requires Coolify
842
+ # v4.0.0-beta.411 or newer, which is when magic variables in a compose file
843
+ # from a Git source arrived.
844
+ services:
845
+ postgres:
846
+ image: postgres:18-alpine@sha256:d3e1620b530c944afa6e887d22eb899824da68e19c52024bf98f5220c88a65b2
847
+ restart: unless-stopped
848
+ mem_limit: \${POSTGRES_MEM_LIMIT:-1g}
849
+ cpus: \${POSTGRES_CPUS:-1}
850
+ environment:
851
+ POSTGRES_USER: community
852
+ POSTGRES_PASSWORD: $SERVICE_PASSWORD_POSTGRES
853
+ POSTGRES_DB: community
854
+ volumes:
855
+ - pgdata:/var/lib/postgresql
856
+ healthcheck:
857
+ test: ['CMD-SHELL', 'pg_isready -U community -d community']
858
+ interval: 10s
859
+ timeout: 5s
860
+ retries: 5
861
+
862
+ # Runs to completion, then exits. web waits for it, so the schema is
863
+ # always applied before the first request rather than racing it.
864
+ migrate:
865
+ build: .
866
+ image: ${name}
867
+ environment:
868
+ MEITH_ROLE: migrate
869
+ DATABASE_URL: postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
870
+ AUTH_SECRET: $SERVICE_BASE64_64_AUTH
871
+ TICK_SECRET: $SERVICE_BASE64_64_TICK
872
+ depends_on:
873
+ postgres:
874
+ condition: service_healthy
875
+ restart: 'no'
876
+
877
+ web:
878
+ build: .
879
+ image: ${name}
880
+ restart: unless-stopped
881
+ mem_limit: \${WEB_MEM_LIMIT:-1g}
882
+ cpus: \${WEB_CPUS:-2}
883
+ # A readiness probe Coolify gates a rolling deploy on: with "Rolling
884
+ # update" enabled on the resource, the new container must answer
885
+ # /api/ready before the old one is retired, so a redeploy swaps in with no
886
+ # gap. Without it Coolify recreates the stack \u2014 old removed, then new
887
+ # started \u2014 and the board is down while the new web boots.
888
+ healthcheck:
889
+ 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))"]
890
+ interval: 30s
891
+ timeout: 5s
892
+ start_period: 20s
893
+ retries: 3
894
+ environment:
895
+ # Ask Coolify for a domain on port 3000, then hand the board the same
896
+ # thing with a scheme in front.
897
+ - SERVICE_FQDN_WEB_3000
898
+ - APP_URL=$SERVICE_URL_WEB
899
+ - DATABASE_URL=postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
900
+ - AUTH_SECRET=$SERVICE_BASE64_64_AUTH
901
+ - TICK_SECRET=$SERVICE_BASE64_64_TICK
902
+ - QUEUE_DRIVER=postgres
903
+ - CACHE_DRIVER=next
904
+ - FILESTORE_DRIVER=local
905
+ # Left unset, mail is configured on the board itself \u2014 the installer
906
+ # asks on first run. Set MAIL_DRIVER here and this file wins instead.
907
+ - MAIL_DRIVER=\${MAIL_DRIVER:-log}
908
+ - MAIL_SMTP_HOST=\${MAIL_SMTP_HOST:-}
909
+ - MAIL_SMTP_PORT=\${MAIL_SMTP_PORT:-}
910
+ - MAIL_SMTP_SECURITY=\${MAIL_SMTP_SECURITY:-}
911
+ - MAIL_SMTP_USERNAME=\${MAIL_SMTP_USERNAME:-}
912
+ - MAIL_SMTP_PASSWORD=\${MAIL_SMTP_PASSWORD:-}
913
+ - MAIL_FROM=\${MAIL_FROM:-}
914
+ volumes:
915
+ - uploads:/app/.uploads
916
+ depends_on:
917
+ postgres:
918
+ condition: service_healthy
919
+ migrate:
920
+ condition: service_completed_successfully
921
+
922
+ # @meith/worker is not published (see the meith repository's
923
+ # docs/contributing/release.md), so there is no compiled worker binary a scaffolded
924
+ # board can run \u2014 this drives the tick the alternative way the meith
925
+ # repository documents in docs/getting-started/deployment/docker-compose.md, "Running the tick without
926
+ # a second set of credentials": a small loop calling /api/system/tick.
927
+ worker:
928
+ image: alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
929
+ restart: unless-stopped
930
+ mem_limit: \${WORKER_MEM_LIMIT:-64m}
931
+ cpus: \${WORKER_CPUS:-0.25}
932
+ environment:
933
+ TICK_SECRET: $SERVICE_BASE64_64_TICK
934
+ command:
935
+ - sh
936
+ - -c
937
+ - |
938
+ apk add --no-cache curl >/dev/null
939
+ while true; do
940
+ curl -fsS -m 55 -H "Authorization: Bearer $$TICK_SECRET" \\
941
+ http://web:3000/api/system/tick >/dev/null 2>&1 \\
942
+ || echo "tick failed at $$(date -Is)"
943
+ sleep 60
944
+ done
945
+ depends_on:
946
+ - web
947
+
948
+ volumes:
949
+ pgdata:
950
+ uploads:
951
+ `
952
+ );
953
+ files.set(
954
+ "docker-compose.prebuilt.yaml",
955
+ `# ${name}, deployed by Coolify from a prebuilt image \u2014 the advanced path: point
956
+ # Coolify's compose-file at this file instead of docker-compose.yaml once
957
+ # \`.github/workflows/build.yml\` has pushed an image, and set MEITH_IMAGE to
958
+ # what its Summary printed. This trades the quick-start's heavier
959
+ # build-on-every-deploy for a low-spec build server or a faster deploy \u2014 see
960
+ # README.md, "Deploy".
961
+ #
962
+ # Same shape as the meith repository's own docker/compose.coolify.yml: db,
963
+ # migrate, web, worker.
750
964
  #
751
965
  # No published ports \u2014 Coolify's proxy routes to the container and issues
752
966
  # the certificate. The two secrets and the database password are Coolify's
@@ -778,8 +992,12 @@ services:
778
992
  # always applied before the first request rather than racing it.
779
993
  migrate:
780
994
  image: \${MEITH_IMAGE:?set this to the image the build workflow's Summary just printed, e.g. ghcr.io/<you>/${name}:latest}
995
+ # Pull the tag on every deploy. Compose keeps an image it already has, so
996
+ # a rebuilt \`:latest\` is otherwise never fetched and a redeploy quietly
997
+ # runs the old code.
998
+ pull_policy: always
781
999
  environment:
782
- COMMUNITY_ROLE: migrate
1000
+ MEITH_ROLE: migrate
783
1001
  DATABASE_URL: postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
784
1002
  AUTH_SECRET: $SERVICE_BASE64_64_AUTH
785
1003
  TICK_SECRET: $SERVICE_BASE64_64_TICK
@@ -790,9 +1008,21 @@ services:
790
1008
 
791
1009
  web:
792
1010
  image: \${MEITH_IMAGE:?set this to the image the build workflow's Summary just printed, e.g. ghcr.io/<you>/${name}:latest}
1011
+ pull_policy: always
793
1012
  restart: unless-stopped
794
1013
  mem_limit: \${WEB_MEM_LIMIT:-1g}
795
1014
  cpus: \${WEB_CPUS:-2}
1015
+ # A readiness probe Coolify gates a rolling deploy on: with "Rolling
1016
+ # update" enabled on the resource, the new container must answer
1017
+ # /api/ready before the old one is retired, so a redeploy swaps in with no
1018
+ # gap. Without it Coolify recreates the stack \u2014 old removed, then new
1019
+ # started \u2014 and the board is down while the new web boots.
1020
+ healthcheck:
1021
+ 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))"]
1022
+ interval: 30s
1023
+ timeout: 5s
1024
+ start_period: 20s
1025
+ retries: 3
796
1026
  environment:
797
1027
  # Ask Coolify for a domain on port 3000, then hand the board the same
798
1028
  # thing with a scheme in front.
@@ -860,15 +1090,55 @@ A forum, built on [Meith](${repositoryUrl}).
860
1090
 
861
1091
  ## Deploy
862
1092
 
863
- Nothing here builds on your own server \u2014 a 2 GB VPS OOMs on a Next.js build,
864
- which is the whole reason \`Dockerfile\`, \`docker-compose.yaml\` and
865
- \`.github/workflows/build.yml\` exist: something else builds the image, the
866
- server only ever pulls one. Three steps, nothing to configure by hand beyond
867
- one value only you know:
1093
+ Two paths onto [Coolify](https://coolify.io), both ending at the same
1094
+ \`/install\`. **Quick start** is the default and needs nothing but a push;
1095
+ **advanced/prebuilt** moves the build off the server, onto GitHub Actions, for
1096
+ a low-spec build server or a faster deploy. Pick one \u2014 a board only ever runs
1097
+ one of them at a time.
1098
+
1099
+ ### Quick start (default)
1100
+
1101
+ Coolify builds the image itself, from this repository, every time it
1102
+ deploys \u2014 there is nothing to push anywhere first and no image tag to paste
1103
+ in. Two steps:
1104
+
1105
+ 1. **Push this repository to GitHub.**
1106
+
1107
+ 2. **Point Coolify at \`docker-compose.yaml\`** \u2014 a **Public Git repository**
1108
+ resource with **Docker Compose** as its build pack, this repository as its
1109
+ source. The name is Coolify's own default, so its **Compose file** field is
1110
+ already right when the form opens, and the file already carries Coolify's
1111
+ own "magic variables" for \`AUTH_SECRET\`, \`TICK_SECRET\` and the database
1112
+ password, generated on the first deploy and never typed in. Nothing else to
1113
+ set: \`docker-compose.yaml\` builds \`web\` and \`migrate\` from \`Dockerfile\`
1114
+ itself, so there is no \`MEITH_IMAGE\` here at all.
1115
+
1116
+ 3. **Deploy, then \`/install\` on your own domain.** Coolify issues the
1117
+ certificate; the installer from there is the one
1118
+ [docs/getting-started/deployment/coolify.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/coolify.md#4-run-the-installer)
1119
+ walks through, screen for screen. It seals itself when it finishes, and
1120
+ \`/install\` answers 404 from then on \u2014 run it **against the database you
1121
+ are going to keep**. Every push to \`main\` after this is picked up the next
1122
+ time Coolify's own **Redeploy** button runs \u2014 pushing alone does not
1123
+ rebuild it.
1124
+
1125
+ The trade for that zero setup is a heavier build: \`Dockerfile\` installs this
1126
+ board's full dependency closure on the server itself, on every deploy, rather
1127
+ than starting from a warm base image. A 2 GB VPS can OOM on it. If that is
1128
+ your server, use the advanced path below instead.
1129
+
1130
+ A quick-start board never needs \`Dockerfile.prebuilt\`,
1131
+ \`docker-compose.prebuilt.yaml\` or \`.github/workflows/build.yml\` \u2014 delete all
1132
+ three.
1133
+
1134
+ ### Advanced / prebuilt \u2014 for a low-spec server or a faster deploy
1135
+
1136
+ Something else builds the image ahead of time; the server only ever pulls
1137
+ one. Three steps, nothing to configure by hand beyond one value only you know:
868
1138
 
869
1139
  1. **Push this repository to GitHub.** \`.github/workflows/build.yml\` builds
870
- \`Dockerfile\` on every push to \`main\` and pushes the result to your own
871
- GitHub Container Registry, \`ghcr.io/<you>/${name}\` \u2014 using only the
1140
+ \`Dockerfile.prebuilt\` on every push to \`main\` and pushes the result to your
1141
+ own GitHub Container Registry, \`ghcr.io/<you>/${name}\` \u2014 using only the
872
1142
  \`GITHUB_TOKEN\` every GitHub Actions run already carries. No secret to
873
1143
  add, no registry account beyond the GitHub account you already have.
874
1144
 
@@ -879,29 +1149,26 @@ one value only you know:
879
1149
  repository usually lands public already, and a private one fails
880
1150
  Coolify's pull with an authentication error no operator can act on.
881
1151
 
882
- 2. **Point [Coolify](https://coolify.io) at \`docker-compose.yaml\`** \u2014 a
1152
+ 2. **Point Coolify at \`docker-compose.prebuilt.yaml\`** \u2014 a
883
1153
  **Public Git repository** resource with **Docker Compose** as its build
884
- pack, this repository as its source. The name is Coolify's own default,
885
- so its **Compose file** field is already right when the form opens, and
886
- the file already carries Coolify's own "magic variables" for
887
- \`AUTH_SECRET\`, \`TICK_SECRET\` and the database password, generated on
888
- the first deploy and never typed in. The one thing Coolify cannot
889
- generate is the image step 1 just pushed: set \`MEITH_IMAGE\` in the
1154
+ pack, this repository as its source, and its **Compose file** field
1155
+ changed from Coolify's default of \`docker-compose.yaml\` to
1156
+ \`docker-compose.prebuilt.yaml\`. That file carries Coolify's own "magic
1157
+ variables" for \`AUTH_SECRET\`, \`TICK_SECRET\` and the database password,
1158
+ generated on the first deploy and never typed in. The one thing Coolify
1159
+ cannot generate is the image step 1 just pushed: set \`MEITH_IMAGE\` in the
890
1160
  resource's own environment to one of the two values that run's Summary
891
- printed (\`docker-compose.yaml\` refuses to start without it, with a
892
- message saying why). \`ghcr.io/<you>/${name}:\${{ github.sha }}\` names
1161
+ printed (\`docker-compose.prebuilt.yaml\` refuses to start without it, with
1162
+ a message saying why). \`ghcr.io/<you>/${name}:\${{ github.sha }}\` names
893
1163
  that one build and nothing else, ever; \`ghcr.io/<you>/${name}:latest\`
894
1164
  follows \`main\` instead, so installing a plugin later is a push and a
895
- **Redeploy** \u2014 the trade the quickstart takes, at the cost of an
896
- unrelated redeploy pulling whatever \`main\` most recently built.
1165
+ **Redeploy** \u2014 the trade this path takes, at the cost of an unrelated
1166
+ redeploy pulling whatever \`main\` most recently built.
897
1167
 
898
- 3. **Deploy, then \`/install\` on your own domain.** Coolify issues the
899
- certificate; the installer from there is the one
1168
+ 3. **Deploy, then \`/install\` on your own domain.** Same installer, same
900
1169
  [docs/getting-started/deployment/coolify.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/coolify.md#4-run-the-installer)
901
- walks through, screen for screen. It seals itself when it finishes, and
902
- \`/install\` answers 404 from then on \u2014 run it **against the database you
903
- are going to keep**. Every push to \`main\` after this rebuilds the
904
- image; Coolify's own **Redeploy** button is what actually pulls it \u2014
1170
+ walk-through, same one-time seal. Every push to \`main\` after this rebuilds
1171
+ the image; Coolify's own **Redeploy** button is what actually pulls it \u2014
905
1172
  pushing alone does not.
906
1173
 
907
1174
  No Docker Hub, no paid CI: GitHub Actions' free tier and GHCR are the whole
@@ -909,26 +1176,27 @@ build side of this, for a board of any size.
909
1176
 
910
1177
  **Building it yourself**: works on any machine with Docker, if you would
911
1178
  rather not use GitHub Actions for the build \u2014 push the result wherever
912
- \`docker-compose.yaml\`'s \`MEITH_IMAGE\` can reach.
1179
+ \`docker-compose.prebuilt.yaml\`'s \`MEITH_IMAGE\` can reach.
913
1180
 
914
1181
  \`\`\`sh
915
- docker build --build-arg MEITH_VERSION=$(node -p "require('./package.json').dependencies['@meith/web']") -t ${name} .
1182
+ docker build -f Dockerfile.prebuilt --build-arg MEITH_VERSION=$(node -p "require('./package.json').dependencies['@meith/web']") -t ${name} .
916
1183
  \`\`\`
917
1184
 
918
1185
  **Without a panel**: [docs/getting-started/deployment/docker-compose.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/docker-compose.md)
919
1186
  is the same four containers by hand \u2014 your own \`.env\`, a reverse proxy you
920
1187
  already run, no Coolify. \`Dockerfile\` and \`docker-compose.yaml\` here are this
921
- board's own version of exactly that shape.
1188
+ board's own version of exactly that shape (or \`Dockerfile.prebuilt\` and
1189
+ \`docker-compose.prebuilt.yaml\`, for the advanced path).
922
1190
 
923
- Two things nothing configures for you:
1191
+ Two things nothing configures for you, on either path:
924
1192
 
925
1193
  - **Mail.** Until \`MAIL_DRIVER\` and its three settings exist, every message is
926
1194
  written to the log and delivered to nobody, so password reset fails silently.
927
- - **The tick.** \`docker-compose.yaml\`'s \`worker\` service drives it here \u2014 a small
1195
+ - **The tick.** The compose file's \`worker\` service drives it here \u2014 a small
928
1196
  loop calling \`/api/system/tick\` once a minute, since \`@meith/web\`'s own
929
1197
  worker package is not something a board outside the meith monorepo can
930
1198
  depend on yet. Deploy some other way and something still has to call that
931
- route (or run \`community task:run\`) every minute, or nothing catches up
1199
+ route (or run \`meith task:run\`) every minute, or nothing catches up
932
1200
  and nothing errors.
933
1201
 
934
1202
  ## Local
@@ -946,8 +1214,8 @@ Posting needs Postgres. Copy \`.env.example\` to \`.env.local\`, set
946
1214
  \`DATABASE_URL\` and the two secrets in it, then:
947
1215
 
948
1216
  \`\`\`sh
949
- npm run community -- migrate
950
- echo "<password>" | npm run community -- user:create --username <name> --email <address> --group administrators
1217
+ npm run meith -- migrate
1218
+ echo "<password>" | npm run meith -- user:create --username <name> --email <address> --group administrators
951
1219
  \`\`\`
952
1220
 
953
1221
  ## Configuring
@@ -958,9 +1226,51 @@ echo "<password>" | npm run community -- user:create --username <name> --email <
958
1226
  - **\`/admin\`** \u2014 settings, forums, groups, members, themes, maintenance. An
959
1227
  administrator re-enters their password to get in, and again for anything
960
1228
  destructive.
961
- - **\`npm run community -- --help\`** \u2014 the operator CLI. Everything the panel does
1229
+ - **\`npm run meith -- --help\`** \u2014 the operator CLI. Everything the panel does
962
1230
  and a few things it cannot, without a browser.
963
1231
 
1232
+ ## Installing plugins and themes
1233
+
1234
+ Nothing installs into a running container \u2014 a plugin or theme has to be
1235
+ built into the image, the same as any other dependency:
1236
+
1237
+ 1. **In this repository**, install it:
1238
+
1239
+ \`\`\`sh
1240
+ npm install --save-exact @meith/plugin-dues
1241
+ \`\`\`
1242
+
1243
+ (a theme is the same command with its own package, e.g.
1244
+ \`@meith/theme-midnight\`).
1245
+
1246
+ 2. **Register it.** A **theme** goes in \`meith.config.ts\`, in the \`themes\`
1247
+ map, following the shape of the \`default\` entry already there. A
1248
+ **plugin** goes in \`meith.plugins.ts\`: import its \`plugin\` and
1249
+ \`messages\` exports and add \`{ key, enabled: true, plugin, messages }\`
1250
+ to \`INSTALLED_PLUGINS\` \u2014 or run
1251
+
1252
+ \`\`\`sh
1253
+ npm run meith -- plugin:add @meith/plugin-dues
1254
+ \`\`\`
1255
+
1256
+ which edits \`board.plugins.json\` and regenerates \`meith.plugins.ts\`
1257
+ for you.
1258
+
1259
+ 3. **Commit and push**, then **Redeploy** from Coolify \u2014 pushing alone does
1260
+ not rebuild. Quick start builds the new image on that redeploy; advanced/prebuilt
1261
+ waits for \`.github/workflows/build.yml\` to finish first, and Redeploy is
1262
+ what actually pulls the result.
1263
+
1264
+ 4. **Once it is up, run its migrations one time:**
1265
+
1266
+ \`\`\`sh
1267
+ docker compose run --rm web meith upgrade
1268
+ \`\`\`
1269
+
1270
+ See [docs/customization/plugins.md](${repositoryUrl}/blob/main/docs/customization/plugins.md)
1271
+ and [docs/customization/themes.md](${repositoryUrl}/blob/main/docs/customization/themes.md)
1272
+ for the full reference.
1273
+
964
1274
  ## Upgrading
965
1275
 
966
1276
  \`\`\`sh
@@ -985,19 +1295,23 @@ weekly pull request bumping the actions pinned in
985
1295
  \`.github/workflows/build.yml\`, which is a safe, independent update the two
986
1296
  commands above never touch.
987
1297
 
988
- That \`package.json\` change is the whole pin: \`Dockerfile\`'s own
989
- \`FROM\` line takes the version as a build argument, and
990
- \`.github/workflows/build.yml\` reads it straight out of \`package.json\`'s
991
- own \`@meith/web\` dependency when it rebuilds \u2014 nothing in \`Dockerfile\`
992
- itself to keep in sync by hand. \`--save-exact\` matters: npm's default
993
- \`save-prefix\` is \`^\`, and a caret range is not a legal Docker image tag \u2014
994
- without it, this exact command would write \`"^0.18.0"\` and the next build
995
- would fail with \`invalid reference format\` instead of building. This
1298
+ On the quick-start path there is no version to keep in sync by hand:
1299
+ \`Dockerfile\` runs \`npm install\` straight from this \`package.json\` on every
1300
+ build, so a rebuild always picks up whatever is pinned there. On the
1301
+ advanced/prebuilt path, that \`package.json\` change is the whole pin:
1302
+ \`Dockerfile.prebuilt\`'s own \`FROM\` line takes the version as a build argument,
1303
+ and \`.github/workflows/build.yml\` reads it straight out of \`package.json\`'s
1304
+ own \`@meith/web\` dependency when it rebuilds \u2014 nothing in
1305
+ \`Dockerfile.prebuilt\` itself to keep in sync by hand. \`--save-exact\` matters
1306
+ either way: npm's default \`save-prefix\` is \`^\`, and a caret range is not a
1307
+ legal Docker image tag for the advanced path \u2014 without it, this exact command
1308
+ would write \`"^0.18.0"\` and the next \`Dockerfile.prebuilt\` build would fail
1309
+ with \`invalid reference format\` instead of building. This
996
1310
  project's own \`.npmrc\` sets \`save-exact=true\` for the same reason, so an
997
1311
  \`npm install\` of anything else here \u2014 a plugin, say \u2014 stays pinned too; the
998
1312
  build workflow also refuses to build from anything but an exact version, as
999
1313
  a second line of defense. Once the rebuilt image is deployed, run
1000
- \`npm run community -- upgrade\` against it for the plugin migrations \u2014 see
1314
+ \`npm run meith -- upgrade\` against it for the plugin migrations \u2014 see
1001
1315
  [the operator CLI](${repositoryUrl}/blob/main/docs/guides/operations/operating.md#the-operator-cli)
1002
1316
  for running it against this deployment.
1003
1317
 
@@ -1240,9 +1554,9 @@ A board must stay movable, and the Blob store is the one part of this shape that
1240
1554
  is not portable: Neon and Upstash hand out ordinary Postgres and Redis strings
1241
1555
  that any host accepts, but a Vercel Blob store is reachable only through Vercel's
1242
1556
  own API and there is no bucket to sync out of it. **The uploads are the thing you
1243
- have to carry out deliberately, and \`community backup\` is how.**
1557
+ have to carry out deliberately, and \`meith backup\` is how.**
1244
1558
 
1245
- Under \`FILESTORE_DRIVER=blob\`, \`community backup\` includes the uploads **by
1559
+ Under \`FILESTORE_DRIVER=blob\`, \`meith backup\` includes the uploads **by
1246
1560
  default** \u2014 it walks the Blob store, pulls every object, and puts them in the
1247
1561
  bundle beside the database dump. This is the opposite of the \`s3\` default, which
1248
1562
  skips them, because a bucket has its own backup story you can drive yourself and
@@ -1253,7 +1567,7 @@ DATABASE_URL=\u2026 # Neon's pooled string
1253
1567
  DIRECT_DATABASE_URL=\u2026 # Neon's DATABASE_URL_UNPOOLED
1254
1568
  FILESTORE_DRIVER=blob
1255
1569
  BLOB_READ_WRITE_TOKEN=\u2026 # create one on the store; see below
1256
- npm run community -- backup
1570
+ npm run meith -- backup
1257
1571
  \`\`\`
1258
1572
 
1259
1573
  Run that from a checkout of this repository, with those four values in the
@@ -1274,10 +1588,10 @@ so the same bundle moves the board either onward or away:
1274
1588
 
1275
1589
  \`\`\`sh
1276
1590
  # onto a self-hosted board with a bucket
1277
- FILESTORE_DRIVER=s3 S3_BUCKET=\u2026 RESTORE_DATABASE_URL=\u2026 npm run community -- restore bundle.tar.gz
1591
+ FILESTORE_DRIVER=s3 S3_BUCKET=\u2026 RESTORE_DATABASE_URL=\u2026 npm run meith -- restore bundle.tar.gz
1278
1592
 
1279
1593
  # onto a board that keeps uploads on its own disk
1280
- RESTORE_DATABASE_URL=\u2026 npm run community -- restore bundle.tar.gz --uploads-dir ./uploads
1594
+ RESTORE_DATABASE_URL=\u2026 npm run meith -- restore bundle.tar.gz --uploads-dir ./uploads
1281
1595
  \`\`\`
1282
1596
 
1283
1597
  Take one before you need it. A Blob store deleted with the Vercel project takes
@@ -1621,7 +1935,7 @@ and add the matching entry to \`board.plugins.json\`:
1621
1935
  { "plugins": [{ "key": "${name}", "package": "${name}", "enabled": true }] }
1622
1936
 
1623
1937
  Rebuild the board (\`npm run build\`) and, because this plugin ships a
1624
- migration, run \`npx community migrate\`. The plugin then appears under
1938
+ migration, run \`npx meith migrate\`. The plugin then appears under
1625
1939
  **Admin \u2192 Plugins**.
1626
1940
 
1627
1941
  ## Publish and list it
@@ -1880,7 +2194,7 @@ async function run(argv, version) {
1880
2194
  ...kind === null ? [
1881
2195
  "Then set DATABASE_URL, AUTH_SECRET and TICK_SECRET and deploy.",
1882
2196
  "Something must run the tick every minute \u2014 the worker process, or",
1883
- "`community task:run`. Without it nothing catches up, and nothing errors."
2197
+ "`meith task:run`. Without it nothing catches up, and nothing errors."
1884
2198
  ] : [
1885
2199
  `Then follow README.md \u2014 it walks through running the ${kind} inside a`,
1886
2200
  "scaffolded board and submitting it to the meith.dev marketplace."
@@ -1890,7 +2204,7 @@ async function run(argv, version) {
1890
2204
  }
1891
2205
 
1892
2206
  // src/bin.ts
1893
- var result = await run(process.argv.slice(2), "0.26.0");
2207
+ var result = await run(process.argv.slice(2), "0.27.0");
1894
2208
  for (const line of result.lines) {
1895
2209
  if (result.code === 0) console.log(line);
1896
2210
  else console.error(line);