diff --git a/README.md b/README.md index 1805fbf49..6e0fe971c 100644 --- a/README.md +++ b/README.md @@ -1,137 +1,56 @@ # Meith -**Built for communities. Owned by them.** Meith is open-source, self-hosted forum software for conversations worth keeping: built on modern TypeScript, configured from a repository you own, and fast enough to run with JavaScript disabled. Threaded discussions, search, permissions, moderation, paid memberships, themes, plugins and an operator CLI — from a single config repo to a server of your own, with no per-member pricing. +Open-source forum software for communities that want to run their own board. +Meith includes discussions, search, moderation, themes, and plugins, with most +reading and posting available without JavaScript. -- [Website](https://meith.dev) -- [Live demo](https://demo.meith.dev) -- [Documentation](./docs/README.md) -- [Issue tracker](https://github.com/meith-dev/meith/issues) +[Website](https://www.meith.dev) · [Live demo](https://demo.meith.dev) · +[Documentation](./docs/README.md) · [Issues](https://github.com/meith-dev/meith/issues) -## Try it in a minute +## Try it locally + +You need **Node.js 22 or newer**. ```sh npx create-meith my-board -cd my-board && npm install && npm run dev +cd my-board +npm install +npm run dev ``` -That is a board of your own on , serving a realistic fixture dataset from memory — no database, no Docker. [The quickstart](./docs/getting-started/quickstart.md) is this route written out; [the introduction](./docs/getting-started/introduction.md) is the philosophy behind it. - -## Run a board +Open . This preview uses sample data and needs no +database or Docker. Saving posts requires PostgreSQL; the +[quickstart](./docs/getting-started/quickstart.md) explains the next step. -[Deployment](./docs/getting-started/deployment/index.md) has three routes onto your own infrastructure: [guided with Coolify](./docs/getting-started/deployment/coolify.md) for most boards, [Docker Compose by hand](./docs/getting-started/deployment/docker-compose.md) if you already operate a proxy, or [Vercel](./docs/getting-started/deployment/vercel.md) if you would rather not have a server. +## Run a community -A production board runs four services: +[Choose a deployment](./docs/getting-started/deployment/index.md): Coolify, +Docker Compose, or Vercel. After installation, +[set up your community](./docs/getting-started/first-steps.md). -- PostgreSQL stores board data. -- A one-shot migration service updates the schema before startup. -- The web service handles browser and API requests. -- The worker runs scheduled and queued work once a minute. +Already running MyBB or phpBB? Start with the +[migration guide](./docs/guides/migrating.md). -After deployment, open `/install` to create the board and its first administrator. +## Contribute -### Work on Meith - -You need Node.js 22 or newer and pnpm 10. +To work on Meith itself, you need **Node.js 22 or newer** and **pnpm 10**. ```sh +git clone https://github.com/meith-dev/meith.git +cd meith pnpm install pnpm dev ``` -`pnpm dev` starts the community app at in fixture mode, so you can browse the interface without PostgreSQL. See [Development](./docs/contributing/development.md) before changing data or opening a pull request. - -The main workspace applications are: - -| Path | Package | Purpose | -|---|---|---| -| `apps/community` | `@meith/web` | The forum application | -| `apps/worker` | `@meith/worker` | Background and scheduled work | -| `apps/cli` | `@meith/cli` | Operator commands | -| `apps/web` | `@meith/site` | meith.dev and the documentation site | - -Business capabilities live in `packages/`. Themes, plugins, examples, deployment files, and documentation have their own top-level directories. [Architecture](./docs/reference/architecture.md) explains the boundaries and request flow. - -## Documentation - -Start with the task you need to complete: - -| Task | Guide | -|---|---| -| See it running locally | [Quickstart](./docs/getting-started/quickstart.md) | -| Install a board | [Deployment](./docs/getting-started/deployment/index.md) | -| Configure a board in code | [Configuration](./docs/guides/configuration.md) | -| Move from MyBB or phpBB | [Migrating](./docs/guides/migrating.md) | -| Administer forums and members | [Organiser guide](./docs/guides/community/organiser-guide.md) | -| Moderate content | [Moderator guide](./docs/guides/community/moderation-guide.md) | -| Manage paid memberships | [Memberships guide](./docs/guides/community/membership-guide.md) | -| Operate the server | [Operations](./docs/guides/operations/operating.md) | -| Upgrade or recover a board | [Upgrading](./docs/guides/operations/upgrading.md) · [Disaster recovery](./docs/guides/operations/disaster-recovery.md) | -| Build a theme or plugin | [Themes](./docs/customization/themes.md) · [Plugins](./docs/customization/plugins.md) | -| Contribute to Meith | [Development](./docs/contributing/development.md) | - -The complete index is in [`docs/README.md`](./docs/README.md). The website renders these Markdown files directly; there is no separate copy. - -The table below is generated from `apps/web/content/docs.manifest.json` by `pnpm site:docs`. - - - -| Section | Document | What it answers | -|---|---|---| -| Getting started | [`getting-started/introduction.md`](./docs/getting-started/introduction.md) | The philosophy: server-first pages that work with JavaScript off, a board pinned in a small repository, and a server of your own. Start here to see whether Meith thinks like you do. | -| Getting started | [`getting-started/quickstart.md`](./docs/getting-started/quickstart.md) | npx create-meith, and a minute later a board is running on your machine in fixture mode — no database, no Docker, no account with anybody. | -| Getting started | [`getting-started/deployment/index.md`](./docs/getting-started/deployment/index.md) | The four-service production shape every route deploys, and the chooser: Coolify guided, Compose by hand, or Vercel without a server. | -| Getting started | [`getting-started/deployment/coolify.md`](./docs/getting-started/deployment/coolify.md) | From a rented server to a board on your own domain, with Coolify, in about twenty minutes. Written for whichever volunteer drew the short straw. | -| Getting started | [`getting-started/deployment/docker-compose.md`](./docs/getting-started/deployment/docker-compose.md) | The advanced route: Docker Compose, a `.env` you write, and a reverse proxy you run. Most boards should deploy with Coolify instead. | -| Getting started | [`getting-started/deployment/vercel.md`](./docs/getting-started/deployment/vercel.md) | The board on functions: the driver set, the build command that carries the migration, the cron job that replaces the worker, what previews and rollbacks do to your schema — and how to leave again. | -| Configuration & guides | [`guides/configuration.md`](./docs/guides/configuration.md) | What the board repository pins — the engine version, the themes, the plugins — and the deliberate line between it and everything organisers run from the browser. | -| Configuration & guides | [`guides/migrating.md`](./docs/guides/migrating.md) | The full procedure: what to check first, the import command for each source, what does and does not come across, and what to do after it finishes. | -| Configuration & guides | [`guides/community/organiser-guide.md`](./docs/guides/community/organiser-guide.md) | Running the board from a browser: forums and the organisers' room, the community's name and colours, announcements, members — and handing it all over. | -| Configuration & guides | [`guides/community/moderation-guide.md`](./docs/guides/community/moderation-guide.md) | For the volunteers who keep it civil: the approval queue, reports, tidying threads, warnings and bans — and where each screen lives. | -| Configuration & guides | [`guides/community/membership-guide.md`](./docs/guides/community/membership-guide.md) | Taking memberships through the board: plans, discount codes, gifting, the memberships desk and the ledger — Dues end to end, without a terminal. | -| Configuration & guides | [`guides/community/formatting.md`](./docs/guides/community/formatting.md) | The composer's toolbar and the syntax behind it: highlighted code, spoilers, mention suggestions, link previews, and placing an attachment inline. | -| Configuration & guides | [`guides/community/forums.md`](./docs/guides/community/forums.md) | The board's shape and who may do what inside it: the forum tree, a forum's options, how a permission resolves, the matrix, and what a moderator appointment grants. | -| Configuration & guides | [`guides/community/groups.md`](./docs/guides/community/groups.md) | What a group carries: board-wide permissions, the daily allowances, colours and badges, display groups, the groups a plugin may grant, and the rules that promote members automatically. | -| Configuration & guides | [`guides/community/antispam.md`](./docs/guides/community/antispam.md) | The registration challenge, the rate limits and login counters, the render-time word filter, and the ban filters that turn somebody away before an account exists — what each is worth and what it costs a real visitor. | -| Configuration & guides | [`guides/community/reputation.md`](./docs/guides/community/reputation.md) | Members rating each other: what a rating is worth, the four settings, the two that decide between a one-press thanks and a form, and why the totals cannot drift. | -| Configuration & guides | [`guides/community/search.md`](./docs/guides/community/search.md) | The four controls over the most expensive thing a visitor can ask for: switching search off without losing the index, the shortest word a query may rest on, and the two different limits on how often. | -| Configuration & guides | [`guides/operations/operating.md`](./docs/guides/operations/operating.md) | Routine checks, configuration, maintenance commands, backups, recovery, web push, the cookies and security headers the board serves, and troubleshooting for a running board. | -| Configuration & guides | [`guides/operations/backups.md`](./docs/guides/operations/backups.md) | Taking a backup from the control panel, on a schedule, off the server and before an upgrade; what a bundle carries; and restoring one from the installer or the command line. | -| Configuration & guides | [`guides/operations/monitoring.md`](./docs/guides/operations/monitoring.md) | Liveness versus readiness, the optional Prometheus metrics endpoint and what to alert on, distributed tracing, and shipping logs. | -| Configuration & guides | [`guides/operations/webhooks.md`](./docs/guides/operations/webhooks.md) | Deliver board events to an endpoint you choose: creating a subscription, the topics and payload formats, verifying a delivery's signature, and how retries and dead-lettering work. | -| Configuration & guides | [`guides/operations/upgrading.md`](./docs/guides/operations/upgrading.md) | Moving a board between versions: the upgrade command, how far you can jump, and the behaviour changes each release brings. | -| Configuration & guides | [`guides/operations/disaster-recovery.md`](./docs/guides/operations/disaster-recovery.md) | The runbook for the day the server is gone: what recovery consumes, the order of operations from provisioning to DNS, and the rehearsal that turns it from a hope into a plan. | -| Configuration & guides | [`guides/operations/single-sign-on.md`](./docs/guides/operations/single-sign-on.md) | Two-factor authentication, federated sign-in and passkeys: what each means for your members, how to configure them, and the record of what has opened an account. | -| Configuration & guides | [`guides/operations/internationalisation.md`](./docs/guides/operations/internationalisation.md) | How a page picks its language, how to add one, and how a theme or plugin ships its own words. | -| Configuration & guides | [`guides/operations/scaling.md`](./docs/guides/operations/scaling.md) | Running more than one web container: the Redis cache that keeps them coherent, what already scales, and the step-by-step migration from a single-instance board. | -| Configuration & guides | [`guides/operations/demo-mode.md`](./docs/guides/operations/demo-mode.md) | The self-resetting public demo board that runs at demo.meith.dev — what it changes, and how to run one yourself. | -| Customization | [`customization/installing.md`](./docs/customization/installing.md) | The board admin's guide to adding a plugin or theme: find one, install the package, register it, redeploy, and manage it from the panel. | -| Customization | [`customization/first-plugin.md`](./docs/customization/first-plugin.md) | The walkthrough from an empty directory to a plugin running inside a board and listed on the marketplace: scaffold, change a hook, test it, install it, publish it. | -| Customization | [`customization/themes.md`](./docs/customization/themes.md) | How to write a theme, what a theme may do, and what the API freeze covers. | -| Customization | [`customization/plugins.md`](./docs/customization/plugins.md) | What a plugin is, what it may and may not do, and how a failure is contained. | -| Customization | [`customization/marketplace.md`](./docs/customization/marketplace.md) | The curated, reviewed feed of plugins and themes: the listing schema, the review bar, the trust it does and does not extend, and how to submit or remove one. | -| Reference | [`reference/api.md`](./docs/reference/api.md) | Every endpoint, scope and rate limit, generated from the route registry. *(generated)* | -| Reference | [`reference/theme-slots.md`](./docs/reference/theme-slots.md) | Every slot and every view model, generated from the slot registry. *(generated)* | -| Reference | [`reference/plugin-hooks.md`](./docs/reference/plugin-hooks.md) | Every hook and payload, generated from the hook registry. *(generated)* | -| Reference | [`reference/performance.md`](./docs/reference/performance.md) | The p95 budgets for the hot pages, and what the last recorded run measured against a full-scale board. *(generated)* | -| Reference | [`reference/architecture.md`](./docs/reference/architecture.md) | How Meith fits together: the processes, the layers, the path a request takes, and the extension seams. | -| Reference | [`reference/mybb-parity.md`](./docs/reference/mybb-parity.md) | Every place Meith deliberately behaves differently from MyBB, with the reasoning and the cost. Read it before promising anyone a like-for-like move. | -| Reference | [`reference/phpbb-parity.md`](./docs/reference/phpbb-parity.md) | Where a Meith board imported from phpBB behaves differently from the one it came from — the ACL, warnings, bans, announcements and polls, and a couple of places phpBB's own model already matches Meith's. | -| Working on Meith | [`contributing/development.md`](./docs/contributing/development.md) | Running the board on your own machine, the workspace layout, the commands, and what to do before opening a pull request. | - - - -## Checks - -Run the full repository gate before opening a pull request: +The forum opens at with sample data. Read +[Development](./docs/contributing/development.md) for the workspace layout, +database setup, tests, and contribution workflow. Follow [AGENTS.md](./AGENTS.md) +and run `pnpm verify` and `pnpm comments:check` before submitting a pull request. -```sh -pnpm verify -``` - -It checks workspace invariants, generated references, formatting, dependency boundaries, types, and tests. Do not edit generated references directly; the documentation index identifies them and names their generators. +To build an extension for your own board, start with +[Write your first plugin](./docs/customization/first-plugin.md) or +[Theme development](./docs/customization/themes.md). ## Licence -Meith is distributed under the [MIT licence](./LICENSE.md). Use it, modify it, and ship it — including in closed-source and commercial work — provided the copyright notice and permission notice travel with it. - -Copyright © 2026 Jordan Harrison and the Meith contributors. +[MIT](./LICENSE.md). Copyright © 2026 Jordan Harrison and the Meith contributors. diff --git a/apps/web/app/docs/[...slug]/page.tsx b/apps/web/app/docs/[...slug]/page.tsx index a90d0c0e6..95c8c7feb 100644 --- a/apps/web/app/docs/[...slug]/page.tsx +++ b/apps/web/app/docs/[...slug]/page.tsx @@ -65,7 +65,7 @@ export default async function DocumentPage({ params }: PageProps) {
- {sourcePath} + Edit this page {entry.generated ? generated from the code : null}
diff --git a/apps/web/app/docs/page.tsx b/apps/web/app/docs/page.tsx index 772ff0b98..343c71b7e 100644 --- a/apps/web/app/docs/page.tsx +++ b/apps/web/app/docs/page.tsx @@ -2,20 +2,20 @@ import type { Metadata } from 'next' import Link from 'next/link' import { site } from '../../src/content/site' -import { docHref, documentsInSection, internalDocuments, sections } from '../../src/docs/registry' +import { docHref, documentsInSection, sections } from '../../src/docs/registry' import { ogImage } from '../../src/og/card' export const metadata: Metadata = { title: 'Documentation', description: - 'Get a board running, configure it in code, operate it, build themes and plugins, and read the generated references.', + 'Set up a board, get started with Meith, and find guides for operating, using, and developing it.', alternates: { canonical: '/docs' }, openGraph: { type: 'website', siteName: site.name, title: `${site.name} documentation`, description: - 'Get a board running, configure it in code, operate it, build themes and plugins, and read the generated references.', + 'Set up a board, get started with Meith, and find guides for operating, using, and developing it.', url: '/docs', images: ogImage('/docs/og/index', `${site.name} documentation`), }, @@ -25,15 +25,23 @@ export default function DocsIndexPage() { return (

Documentation

-

What do you want to do?

+

Meith documentation

- Choose a task below. Start with the primary guide in that section, then use the shorter - references when you need a specific detail. + Installing a board, running a community, or taking part? Start with the section for your + task.

+ +
{sections.map((section) => ( -
+

{section.title}

@@ -50,7 +58,7 @@ export default function DocsIndexPage() { {doc.title} - {doc.generated ? generated : null} + {doc.primary ? Start here : null} ))}
- -
-

- Kept in the repository -

-

- These are working records rather than documentation, and they are only meaningful beside - the plan they are written against. -

- -
- -

- Every page here is rendered from the Markdown in{' '} - - docs/ - {' '} - at build time. There is no second copy to fall behind, and a document is corrected by - editing that file. -

) } diff --git a/apps/web/content/docs.manifest.json b/apps/web/content/docs.manifest.json index 2269dc155..d960ebb62 100644 --- a/apps/web/content/docs.manifest.json +++ b/apps/web/content/docs.manifest.json @@ -1,445 +1,476 @@ { "$comment": [ - "The single place the documentation set is described.", - "", - "The prose itself is never copied here. Every entry points at a file under", - "docs/ at the workspace root, which stays the one editable copy — the site", - "reads those files at build time and renders them. Adding a document means", - "adding it to docs/ and naming it here (or in `internal`), and nothing else.", - "", - "JSON rather than TypeScript so that `scripts/site-docs.mjs` and the Next", - "app read the same bytes. A registry the checker cannot parse is a registry", - "that drifts from what is published.", - "", - "Checked by `pnpm site:docs:check`, which fails when a file under docs/ is", - "in neither list, when an entry points at a file that does not exist, or", - "when README.md's generated table no longer matches this file." + "Documentation source: docs/. Sections control the site navigation, reading order and docs/README.md index.", + "Register every Markdown file here, then run pnpm site:docs to update docs/README.md.", + "Keep published slugs stable when moving documents between sections. Each section has one primary guide." ], "sections": [ { - "id": "getting-started", - "title": "Getting started", - "blurb": "What Meith is, a board running on your machine in about a minute, and the routes onto a server of your own." + "id": "setting-up", + "title": "Setting up", + "blurb": "Install a board, choose a host, or move an existing forum to Meith." }, { - "id": "guides", - "title": "Configuration & guides", - "blurb": "The board repository and its boundary with the admin panel, the move from MyBB or phpBB, and the guides for the people who run the community and the people who mind the server." + "id": "getting-started", + "title": "Getting started with Meith", + "blurb": "Learn the basics and prepare a new community for its first members." }, { - "id": "customization", - "title": "Customization", - "blurb": "The contracts a board is extended through: themes filling documented slots, plugins on typed hooks with crash isolation, and the marketplace they ship in." + "id": "operating", + "title": "Operating Meith", + "blurb": "Administer the community and maintain the services behind it." }, { - "id": "reference", - "title": "Reference", - "blurb": "The generated references — every endpoint, slot, hook and measurement — plus the architecture and the migration appendices." + "id": "using", + "title": "Using Meith", + "blurb": "Join a community, write posts, find discussions, and manage your account." }, { - "id": "contributing", - "title": "Working on Meith", - "blurb": "Running Meith on your own machine, the workspace layout, and the checks a pull request passes." + "id": "developing", + "title": "Developing Meith", + "blurb": "Build themes and plugins, use the API, or contribute to Meith." } ], "documents": [ - { - "slug": "introduction", - "file": "getting-started/introduction.md", - "section": "getting-started", - "title": "Introduction", - "blurb": "The philosophy: server-first pages that work with JavaScript off, a board pinned in a small repository, and a server of your own. Start here to see whether Meith thinks like you do.", - "generated": false, - "primary": false - }, { "slug": "quickstart", "file": "getting-started/quickstart.md", - "section": "getting-started", - "title": "Quickstart", - "blurb": "npx create-meith, and a minute later a board is running on your machine in fixture mode — no database, no Docker, no account with anybody.", + "section": "setting-up", + "title": "Try Meith locally", + "blurb": "Run a local preview without a database.", "generated": false, - "primary": true + "primary": false }, { "slug": "deployment", "file": "getting-started/deployment/index.md", - "section": "getting-started", - "title": "Deployment", - "blurb": "The four-service production shape every route deploys, and the chooser: Coolify guided, Compose by hand, or Vercel without a server.", + "section": "setting-up", + "title": "Choose a deployment", + "blurb": "Compare Coolify, Docker Compose, and Vercel.", "generated": false, - "primary": false + "primary": true }, { "slug": "coolify", "file": "getting-started/deployment/coolify.md", - "section": "getting-started", - "title": "Deploying with Coolify", - "blurb": "From a rented server to a board on your own domain, with Coolify, in about twenty minutes. Written for whichever volunteer drew the short straw.", + "section": "setting-up", + "title": "Deploy with Coolify", + "blurb": "Set up a board on your server through the Coolify panel.", "generated": false, "primary": false, - "group": "Deployment routes" + "group": "Deployment guides" }, { "slug": "docker-compose", "file": "getting-started/deployment/docker-compose.md", - "section": "getting-started", - "title": "Docker Compose by hand", - "blurb": "The advanced route: Docker Compose, a `.env` you write, and a reverse proxy you run. Most boards should deploy with Coolify instead.", + "section": "setting-up", + "title": "Deploy with Docker Compose", + "blurb": "Configure the containers, environment, and reverse proxy yourself.", "generated": false, "primary": false, - "group": "Deployment routes" + "group": "Deployment guides" }, { "slug": "vercel", "file": "getting-started/deployment/vercel.md", - "section": "getting-started", - "title": "Running on Vercel", - "blurb": "The board on functions: the driver set, the build command that carries the migration, the cron job that replaces the worker, what previews and rollbacks do to your schema — and how to leave again.", + "section": "setting-up", + "title": "Deploy on Vercel", + "blurb": "Set up managed services, migrations, and scheduled tasks.", "generated": false, "primary": false, - "group": "Deployment routes" - }, - { - "slug": "configuration", - "file": "guides/configuration.md", - "section": "guides", - "title": "Configuration in code", - "blurb": "What the board repository pins — the engine version, the themes, the plugins — and the deliberate line between it and everything organisers run from the browser.", - "generated": false, - "primary": true + "group": "Deployment guides" }, { "slug": "migrating", "file": "guides/migrating.md", - "section": "guides", - "title": "Migrating from MyBB or phpBB", - "blurb": "The full procedure: what to check first, the import command for each source, what does and does not come across, and what to do after it finishes.", + "section": "setting-up", + "title": "Migrate from MyBB or phpBB", + "blurb": "Rehearse an import, move the data, and check the result.", "generated": false, - "primary": false + "primary": false, + "group": "Moving an existing forum" }, { - "slug": "organiser-guide", - "file": "guides/community/organiser-guide.md", - "section": "guides", - "title": "The organiser's guide", - "blurb": "Running the board from a browser: forums and the organisers' room, the community's name and colours, announcements, members — and handing it all over.", + "slug": "mybb-parity", + "file": "reference/mybb-parity.md", + "section": "setting-up", + "title": "MyBB differences", + "blurb": "Check feature and behaviour differences before migrating.", "generated": false, "primary": false, - "group": "Running your community" + "group": "Moving an existing forum" }, { - "slug": "moderation-guide", - "file": "guides/community/moderation-guide.md", - "section": "guides", - "title": "The moderator's guide", - "blurb": "For the volunteers who keep it civil: the approval queue, reports, tidying threads, warnings and bans — and where each screen lives.", + "slug": "phpbb-parity", + "file": "reference/phpbb-parity.md", + "section": "setting-up", + "title": "phpBB differences", + "blurb": "Check permissions, content, and account differences before migrating.", "generated": false, "primary": false, - "group": "Running your community" + "group": "Moving an existing forum" }, { - "slug": "membership-guide", - "file": "guides/community/membership-guide.md", - "section": "guides", - "title": "The memberships guide", - "blurb": "Taking memberships through the board: plans, discount codes, gifting, the memberships desk and the ledger — Dues end to end, without a terminal.", + "slug": "introduction", + "file": "getting-started/introduction.md", + "section": "getting-started", + "title": "What is Meith?", + "blurb": "Understand boards, forums, threads, and the roles that run them.", "generated": false, - "primary": false, - "group": "Running your community" + "primary": true }, { - "slug": "formatting", - "file": "guides/community/formatting.md", - "section": "guides", - "title": "Writing a post", - "blurb": "The composer's toolbar and the syntax behind it: highlighted code, spoilers, mention suggestions, link previews, and placing an attachment inline.", + "slug": "first-steps", + "file": "getting-started/first-steps.md", + "section": "getting-started", + "title": "Set up your community", + "blurb": "Configure a new board and check it before inviting members.", "generated": false, - "primary": false, - "group": "Running your community" + "primary": false + }, + { + "slug": "organiser-guide", + "file": "guides/community/organiser-guide.md", + "section": "operating", + "title": "Community administration", + "blurb": "Manage forums, appearance, announcements, and members.", + "generated": false, + "primary": true, + "group": "Community administration" }, { "slug": "forums", "file": "guides/community/forums.md", - "section": "guides", + "section": "operating", "title": "Forums and permissions", - "blurb": "The board's shape and who may do what inside it: the forum tree, a forum's options, how a permission resolves, the matrix, and what a moderator appointment grants.", + "blurb": "Arrange forums and control who can read and post.", "generated": false, "primary": false, - "group": "Running your community" + "group": "Community administration" }, { "slug": "groups", "file": "guides/community/groups.md", - "section": "guides", + "section": "operating", "title": "Groups and promotions", - "blurb": "What a group carries: board-wide permissions, the daily allowances, colours and badges, display groups, the groups a plugin may grant, and the rules that promote members automatically.", + "blurb": "Set group permissions, allowances, badges, and promotion rules.", + "generated": false, + "primary": false, + "group": "Community administration" + }, + { + "slug": "moderation-guide", + "file": "guides/community/moderation-guide.md", + "section": "operating", + "title": "Moderation", + "blurb": "Approve posts, handle reports, and manage warnings and bans.", "generated": false, "primary": false, - "group": "Running your community" + "group": "Community administration" }, { "slug": "antispam", "file": "guides/community/antispam.md", - "section": "guides", + "section": "operating", "title": "Spam controls and filters", - "blurb": "The registration challenge, the rate limits and login counters, the render-time word filter, and the ban filters that turn somebody away before an account exists — what each is worth and what it costs a real visitor.", + "blurb": "Configure registration challenges, rate limits, and content filters.", + "generated": false, + "primary": false, + "group": "Community administration" + }, + { + "slug": "search", + "file": "guides/community/search.md", + "section": "operating", + "title": "Search settings", + "blurb": "Configure search language, availability, and query limits.", "generated": false, "primary": false, - "group": "Running your community" + "group": "Community administration" }, { "slug": "reputation", "file": "guides/community/reputation.md", - "section": "guides", - "title": "Reputation", - "blurb": "Members rating each other: what a rating is worth, the four settings, the two that decide between a one-press thanks and a form, and why the totals cannot drift.", + "section": "operating", + "title": "Reputation settings", + "blurb": "Configure ratings, thanks, and who may give reputation.", "generated": false, "primary": false, - "group": "Running your community" + "group": "Community administration" }, { - "slug": "search", - "file": "guides/community/search.md", - "section": "guides", - "title": "Search", - "blurb": "The four controls over the most expensive thing a visitor can ask for: switching search off without losing the index, the shortest word a query may rest on, and the two different limits on how often.", + "slug": "membership-guide", + "file": "guides/community/membership-guide.md", + "section": "operating", + "title": "Paid memberships with Dues", + "blurb": "Configure plans and manage payments, gifts, and the ledger.", "generated": false, "primary": false, - "group": "Running your community" + "group": "Community administration" }, { "slug": "operating", "file": "guides/operations/operating.md", - "section": "guides", - "title": "Operations", - "blurb": "Routine checks, configuration, maintenance commands, backups, recovery, web push, the cookies and security headers the board serves, and troubleshooting for a running board.", + "section": "operating", + "title": "Server operations", + "blurb": "Check services and run routine maintenance commands.", "generated": false, "primary": false, - "group": "Operating the server" + "group": "Server operations" }, { - "slug": "backups", - "file": "guides/operations/backups.md", - "section": "guides", - "title": "Backups", - "blurb": "Taking a backup from the control panel, on a schedule, off the server and before an upgrade; what a bundle carries; and restoring one from the installer or the command line.", + "slug": "configuration", + "file": "guides/configuration.md", + "section": "operating", + "title": "Board configuration", + "blurb": "Understand the build files, runtime settings, and environment.", "generated": false, "primary": false, - "group": "Operating the server" + "group": "Server operations" }, { - "slug": "monitoring", - "file": "guides/operations/monitoring.md", - "section": "guides", - "title": "Monitoring & alerting", - "blurb": "Liveness versus readiness, the optional Prometheus metrics endpoint and what to alert on, distributed tracing, and shipping logs.", + "slug": "installing", + "file": "customization/installing.md", + "section": "operating", + "title": "Install plugins and themes", + "blurb": "Add, update, or remove an extension and deploy the change.", "generated": false, "primary": false, - "group": "Operating the server" + "group": "Server operations" }, { - "slug": "webhooks", - "file": "guides/operations/webhooks.md", - "section": "guides", - "title": "Webhooks", - "blurb": "Deliver board events to an endpoint you choose: creating a subscription, the topics and payload formats, verifying a delivery's signature, and how retries and dead-lettering work.", + "slug": "single-sign-on", + "file": "guides/operations/single-sign-on.md", + "section": "operating", + "title": "Authentication settings", + "blurb": "Configure two-factor authentication, sign-in providers, and passkeys.", + "generated": false, + "primary": false, + "group": "Server operations" + }, + { + "slug": "backups", + "file": "guides/operations/backups.md", + "section": "operating", + "title": "Backups", + "blurb": "Schedule backups, copy them off-site, and test a restore.", "generated": false, "primary": false, - "group": "Operating the server" + "group": "Server operations" }, { "slug": "upgrading", "file": "guides/operations/upgrading.md", - "section": "guides", - "title": "Upgrading a board", - "blurb": "Moving a board between versions: the upgrade command, how far you can jump, and the behaviour changes each release brings.", + "section": "operating", + "title": "Upgrade Meith", + "blurb": "Update the board and apply core and plugin migrations.", "generated": false, "primary": false, - "group": "Operating the server" + "group": "Server operations" }, { "slug": "disaster-recovery", "file": "guides/operations/disaster-recovery.md", - "section": "guides", + "section": "operating", "title": "Disaster recovery", - "blurb": "The runbook for the day the server is gone: what recovery consumes, the order of operations from provisioning to DNS, and the rehearsal that turns it from a hope into a plan.", - "generated": false, - "primary": false, - "group": "Operating the server" - }, - { - "slug": "single-sign-on", - "file": "guides/operations/single-sign-on.md", - "section": "guides", - "title": "Signing in", - "blurb": "Two-factor authentication, federated sign-in and passkeys: what each means for your members, how to configure them, and the record of what has opened an account.", + "blurb": "Restore the database, uploads, and secrets after losing a server.", "generated": false, "primary": false, - "group": "Operating the server" + "group": "Server operations" }, { - "slug": "internationalisation", - "file": "guides/operations/internationalisation.md", - "section": "guides", - "title": "Languages", - "blurb": "How a page picks its language, how to add one, and how a theme or plugin ships its own words.", + "slug": "monitoring", + "file": "guides/operations/monitoring.md", + "section": "operating", + "title": "Monitoring", + "blurb": "Check readiness, scheduler activity, logs, and metrics.", "generated": false, "primary": false, - "group": "Operating the server" + "group": "Server operations" }, { "slug": "scaling", "file": "guides/operations/scaling.md", - "section": "guides", - "title": "Scaling out", - "blurb": "Running more than one web container: the Redis cache that keeps them coherent, what already scales, and the step-by-step migration from a single-instance board.", + "section": "operating", + "title": "Scaling", + "blurb": "Run multiple web instances with shared cache and storage.", "generated": false, "primary": false, - "group": "Operating the server" + "group": "Server operations" }, { "slug": "demo-mode", "file": "guides/operations/demo-mode.md", - "section": "guides", - "title": "Demo mode", - "blurb": "The self-resetting public demo board that runs at demo.meith.dev — what it changes, and how to run one yourself.", + "section": "operating", + "title": "Run a public demo", + "blurb": "Deploy an isolated demo that resets its data on a schedule.", "generated": false, "primary": false, - "group": "Operating the server" + "group": "Server operations" }, { - "slug": "installing", - "file": "customization/installing.md", - "section": "customization", - "title": "Installing plugins and themes", - "blurb": "The board admin's guide to adding a plugin or theme: find one, install the package, register it, redeploy, and manage it from the panel.", + "slug": "member-guide", + "file": "guides/community/member-guide.md", + "section": "using", + "title": "Member guide", + "blurb": "Register, take part in discussions, and manage your account.", "generated": false, "primary": true }, + { + "slug": "formatting", + "file": "guides/community/formatting.md", + "section": "using", + "title": "Write and format posts", + "blurb": "Use Markdown, mentions, spoilers, code, and attachments.", + "generated": false, + "primary": false + }, { "slug": "first-plugin", "file": "customization/first-plugin.md", - "section": "customization", + "section": "developing", "title": "Write your first plugin", - "blurb": "The walkthrough from an empty directory to a plugin running inside a board and listed on the marketplace: scaffold, change a hook, test it, install it, publish it.", + "blurb": "Scaffold, test, and run a plugin in a board.", "generated": false, - "primary": false + "primary": true, + "group": "Build extensions" }, { "slug": "themes", "file": "customization/themes.md", - "section": "customization", - "title": "Themes", - "blurb": "How to write a theme, what a theme may do, and what the API freeze covers.", + "section": "developing", + "title": "Theme development", + "blurb": "Implement theme slots, view models, tokens, and shared controls.", "generated": false, - "primary": false + "primary": false, + "group": "Build extensions" }, { "slug": "plugins", "file": "customization/plugins.md", - "section": "customization", - "title": "Plugins", - "blurb": "What a plugin is, what it may and may not do, and how a failure is contained.", + "section": "developing", + "title": "Plugin development", + "blurb": "Use hooks, UI regions, storage, routes, and scheduled tasks.", "generated": false, - "primary": false + "primary": false, + "group": "Build extensions" + }, + { + "slug": "internationalisation", + "file": "guides/operations/internationalisation.md", + "section": "developing", + "title": "Translations", + "blurb": "Add languages and translate themes and plugins.", + "generated": false, + "primary": false, + "group": "Build extensions" }, { "slug": "marketplace", "file": "customization/marketplace.md", - "section": "customization", - "title": "The marketplace", - "blurb": "The curated, reviewed feed of plugins and themes: the listing schema, the review bar, the trust it does and does not extend, and how to submit or remove one.", + "section": "developing", + "title": "Publish to the marketplace", + "blurb": "Prepare a listing and understand the review requirements.", "generated": false, - "primary": false + "primary": false, + "group": "Build extensions" }, { "slug": "api", "file": "reference/api.md", - "section": "reference", + "section": "developing", "title": "REST API v1", - "blurb": "Every endpoint, scope and rate limit, generated from the route registry.", + "blurb": "Endpoint, scope, and rate-limit reference.", "generated": true, - "primary": true + "primary": false, + "group": "API and reference" + }, + { + "slug": "webhooks", + "file": "guides/operations/webhooks.md", + "section": "developing", + "title": "Webhooks", + "blurb": "Subscribe to events and verify signed deliveries.", + "generated": false, + "primary": false, + "group": "API and reference" }, { "slug": "theme-slots", "file": "reference/theme-slots.md", - "section": "reference", + "section": "developing", "title": "Theme slots and view models", - "blurb": "Every slot and every view model, generated from the slot registry.", + "blurb": "Generated reference for the theme contract.", "generated": true, - "primary": false + "primary": false, + "group": "API and reference" }, { "slug": "plugin-hooks", "file": "reference/plugin-hooks.md", - "section": "reference", + "section": "developing", "title": "Plugin hooks", - "blurb": "Every hook and payload, generated from the hook registry.", + "blurb": "Generated reference for hooks, payloads, and call sites.", "generated": true, - "primary": false + "primary": false, + "group": "API and reference" }, { "slug": "performance", "file": "reference/performance.md", - "section": "reference", + "section": "developing", "title": "Performance", - "blurb": "The p95 budgets for the hot pages, and what the last recorded run measured against a full-scale board.", + "blurb": "Recorded measurements and performance budgets.", "generated": true, - "primary": false + "primary": false, + "group": "API and reference" }, { - "slug": "architecture", - "file": "reference/architecture.md", - "section": "reference", - "title": "Architecture", - "blurb": "How Meith fits together: the processes, the layers, the path a request takes, and the extension seams.", + "slug": "development", + "file": "contributing/development.md", + "section": "developing", + "title": "Contribute to Meith", + "blurb": "Run the repository, make a change, and validate a pull request.", "generated": false, - "primary": false + "primary": false, + "group": "Contribute to Meith" }, { - "slug": "mybb-parity", - "file": "reference/mybb-parity.md", - "section": "reference", - "title": "MyBB parity decisions", - "blurb": "Every place Meith deliberately behaves differently from MyBB, with the reasoning and the cost. Read it before promising anyone a like-for-like move.", + "slug": "architecture", + "file": "reference/architecture.md", + "section": "developing", + "title": "Architecture", + "blurb": "Understand the packages, processes, and request flow.", "generated": false, "primary": false, - "group": "Migration appendices" + "group": "Contribute to Meith" }, { - "slug": "phpbb-parity", - "file": "reference/phpbb-parity.md", - "section": "reference", - "title": "phpBB parity decisions", - "blurb": "Where a Meith board imported from phpBB behaves differently from the one it came from — the ACL, warnings, bans, announcements and polls, and a couple of places phpBB's own model already matches Meith's.", + "slug": "nextjs-conventions", + "file": "contributing/nextjs-conventions.md", + "section": "developing", + "title": "Next.js conventions", + "blurb": "Follow the repository’s routing, rendering, and action patterns.", "generated": false, "primary": false, - "group": "Migration appendices" + "group": "Contribute to Meith" }, { - "slug": "development", - "file": "contributing/development.md", - "section": "contributing", - "title": "Development", - "blurb": "Running the board on your own machine, the workspace layout, the commands, and what to do before opening a pull request.", + "slug": "release", + "file": "contributing/release.md", + "section": "developing", + "title": "Release procedure", + "blurb": "Cut and publish a Meith release.", "generated": false, - "primary": true + "primary": false, + "group": "Contribute to Meith" } ], "internal": [ { "file": "README.md", - "reason": "The index for people reading the repository. On the site, /docs is the index." - }, - { - "file": "contributing/nextjs-conventions.md", - "reason": "Framework conventions enforced inside this repository, read from Development — not documentation a board needs." - }, - { - "file": "contributing/release.md", - "reason": "The maintainers' release procedure. A board follows Upgrading; only this repository cuts releases." + "reason": "Repository index; the site uses /docs." } ] } diff --git a/apps/web/src/components/docs-nav.tsx b/apps/web/src/components/docs-nav.tsx index 00689e26d..979a630b1 100644 --- a/apps/web/src/components/docs-nav.tsx +++ b/apps/web/src/components/docs-nav.tsx @@ -28,43 +28,47 @@ export function DocsNav({ sections }: DocsNavProps) { pathname === '/docs' ? 'text-accent' : 'text-fg-subtle hover:text-fg' }`} > - All documents + Documentation home
    {sections.map((section) => (
  • -

    {section.title}

    -
      - {section.documents.map((doc, index) => { - const href = `/docs/${doc.slug}` - const active = pathname === href - const groupStarts = - doc.group !== undefined && doc.group !== section.documents[index - 1]?.group - return ( -
    • - {groupStarts ? ( -

      - {doc.group} -

      - ) : null} - - {doc.title} - -
    • - ) - })} -
    +
    pathname === `/docs/${doc.slug}`)}> + + {section.title} + +
      + {section.documents.map((doc, index) => { + const href = `/docs/${doc.slug}` + const active = pathname === href + const groupStarts = + doc.group !== undefined && doc.group !== section.documents[index - 1]?.group + return ( +
    • + {groupStarts ? ( +

      + {doc.group} +

      + ) : null} + + {doc.title} + +
    • + ) + })} +
    +
  • ))}
diff --git a/apps/web/src/docs/load.test.ts b/apps/web/src/docs/load.test.ts index 01ac5b255..289cd7d9c 100644 --- a/apps/web/src/docs/load.test.ts +++ b/apps/web/src/docs/load.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { site } from '../content/site' import { linkResolver, loadAllDocuments } from './load' -import { documents, internalDocuments, readingOrder } from './registry' +import { documents, internalDocuments, quickstartHref, readingOrder } from './registry' describe('linkResolver', () => { const fromDocs = linkResolver('guides/operations/operating.md') @@ -76,6 +76,10 @@ describe('linkResolver', () => { }) describe('the published set', () => { + it('keeps the local-preview call to action pointing to the quickstart after regrouping', () => { + expect(quickstartHref()).toBe('/docs/quickstart') + }) + it('renders every document in the manifest', async () => { const loaded = await loadAllDocuments() diff --git a/apps/web/src/docs/registry.ts b/apps/web/src/docs/registry.ts index 5fd80d028..a12275981 100644 --- a/apps/web/src/docs/registry.ts +++ b/apps/web/src/docs/registry.ts @@ -68,9 +68,6 @@ export function docHref(slug: string, anchor?: string): string { } export function quickstartHref(): string { - const gettingStarted = findSection('getting-started') - const quickstart = gettingStarted - ? documentsInSection(gettingStarted.id).find((doc) => doc.primary) - : undefined + const quickstart = findDocument('quickstart') return quickstart ? docHref(quickstart.slug) : '/docs' } diff --git a/docs/README.md b/docs/README.md index 59b2e312c..90f0bf418 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,91 +1,101 @@ # Meith documentation -This directory is the source for [meith.dev/docs](https://www.meith.dev/docs). Choose a guide by task; you do not need to read the set in order. +Choose a section for the task you are doing. Existing members can go straight to the [Member guide](./guides/community/member-guide.md). -## Start here - -| If you want to… | Read… | -|---|---| -| See what Meith is | [Introduction](./getting-started/introduction.md) | -| Run a board on your machine | [Quickstart](./getting-started/quickstart.md) | -| Put a board on your own server | [Deployment](./getting-started/deployment/index.md) | -| Add a plugin or theme to a board | [Installing plugins and themes](./customization/installing.md) | -| Build a theme or plugin | [Write your first plugin](./customization/first-plugin.md) · [Themes](./customization/themes.md) · [Plugins](./customization/plugins.md) | -| Run an existing board from the browser | [Organiser guide](./guides/community/organiser-guide.md) | -| Contribute code | [Development](./contributing/development.md) | - -## Getting started - -- [Introduction](./getting-started/introduction.md) — the philosophy: server-first, code-first, self-hosted. -- [Quickstart](./getting-started/quickstart.md) — a board on your machine in about a minute, no database required. -- [Deployment](./getting-started/deployment/index.md) — the four-service production shape and the route chooser. - - [Coolify](./getting-started/deployment/coolify.md) — the guided route: deploy on your own server from a browser. - - [Docker Compose by hand](./getting-started/deployment/docker-compose.md) — a `.env` you write and a reverse proxy you operate. - - [Vercel](./getting-started/deployment/vercel.md) — the board on functions, and how to leave again. - -## Configuration & guides - -- [Configuration in code](./guides/configuration.md) — the board repository: what is pinned in code, what stays in the admin panel. -- [Migrating from MyBB or phpBB](./guides/migrating.md) — the importer moves members, content, private messages, attachments, subscriptions, polls, reputation, warnings and bans, with working passwords and redirects from the old URLs. - -Running your community — browser only, no shell access: - -- [Organiser guide](./guides/community/organiser-guide.md) — board settings, forums, groups, members, and handover. -- [Moderator guide](./guides/community/moderation-guide.md) — approval, reports, thread maintenance, warnings, and bans. -- [Memberships guide](./guides/community/membership-guide.md) — Stripe plans, discounts, gifts, the membership desk, and the ledger. -- [Writing a post](./guides/community/formatting.md) — the composer's toolbar and its Markdown. -- [Forums and permissions](./guides/community/forums.md) — the forum tree and the per-forum permission matrix. -- [Groups and promotions](./guides/community/groups.md) — what a group carries board-wide. -- [Spam controls and filters](./guides/community/antispam.md) — the registration challenge, every threshold, the render-time word filter, and the ban filters that turn somebody away before an account exists. -- [Reputation](./guides/community/reputation.md) — members rating each other, and the two settings that decide what a post offers. -- [Search](./guides/community/search.md) — switching it off without losing the index, and the two limits on how often. - -Operating the server: - -- [Operations](./guides/operations/operating.md) — health checks, configuration, CLI commands, mail, backups, web push, the cookies and security headers the board serves, and troubleshooting. -- [Backups](./guides/operations/backups.md) — take one from the panel, schedule them, ship them off the server, and restore from the installer. -- [Monitoring & alerting](./guides/operations/monitoring.md) — liveness versus readiness, metrics, tracing, and logs. -- [Webhooks](./guides/operations/webhooks.md) — deliver board events to an endpoint you choose, and verify the signatures. -- [Upgrading](./guides/operations/upgrading.md) — move between released versions safely. -- [Disaster recovery](./guides/operations/disaster-recovery.md) — restore a board when the original server is unavailable. -- [Signing in](./guides/operations/single-sign-on.md) — passwords, two-factor authentication, federated sign-in, passkeys, and sessions. -- [Languages](./guides/operations/internationalisation.md) — locale selection and translation packages. -- [Scaling out](./guides/operations/scaling.md) — add web instances and a shared cache. -- [Demo mode](./guides/operations/demo-mode.md) — run a public board that resets itself. - -## Customization - -- [Installing plugins and themes](./customization/installing.md) — the board admin's guide: find one, install the package, register it, redeploy, and manage it from the panel. -- [Write your first plugin](./customization/first-plugin.md) — the walkthrough from an empty directory to a plugin running inside a board and listed on the marketplace. -- [Themes](./customization/themes.md) — theme slots, view models, and packaging. -- [Plugins](./customization/plugins.md) — plugin boundaries, typed hooks, lifecycle, and crash isolation. -- [The marketplace](./customization/marketplace.md) — the curated feed of plugins and themes, and the listing-by-PR process. - -## Reference - -- [REST API v1](./reference/api.md) — generated routes, scopes, request bodies, and responses. -- `reference/openapi.json` — machine-readable OpenAPI document generated alongside the REST reference. -- [Theme slot reference](./reference/theme-slots.md) — generated slot and view-model reference. -- [Plugin hook reference](./reference/plugin-hooks.md) — generated hook and payload reference. -- [Architecture](./reference/architecture.md) — processes, package boundaries, and request flow. -- [Performance](./reference/performance.md) — generated performance budgets and recorded results. -- [MyBB parity decisions](./reference/mybb-parity.md) and [phpBB parity decisions](./reference/phpbb-parity.md) — intentional product differences from the board you are leaving; appendices to [Migrating](./guides/migrating.md). - -## Contributing - -- [Development](./contributing/development.md) — local setup, tests, and pull-request checks. -- [Next.js conventions](./contributing/nextjs-conventions.md) — application-layer patterns enforced in this repository. -- [Releasing](./contributing/release.md) — versioning and release outputs. - -## Generated references - -Do not edit generated files directly. - -| File | Command | +| Task | Start here | |---|---| -| `reference/theme-slots.md` | `pnpm theme:docs` | -| `reference/plugin-hooks.md` | `pnpm plugin:docs` | -| `reference/api.md`, `reference/openapi.json` | `pnpm api:docs` | -| `reference/performance.md` | `pnpm perf:docs` | +| Install a board | [Choose a deployment](./getting-started/deployment/index.md) | +| Prepare a new community | [Set up your community](./getting-started/first-steps.md) | +| Administer a community | [Community administration](./guides/community/organiser-guide.md) | +| Maintain the server | [Server operations](./guides/operations/operating.md) | +| Build an extension | [Write your first plugin](./customization/first-plugin.md) | +| Contribute to Meith | [Development](./contributing/development.md) | + + + +## Setting up + +Install a board, choose a host, or move an existing forum to Meith. + +- [Try Meith locally](./getting-started/quickstart.md) — Run a local preview without a database. +- [Choose a deployment](./getting-started/deployment/index.md) — Compare Coolify, Docker Compose, and Vercel. +### Deployment guides + +- [Deploy with Coolify](./getting-started/deployment/coolify.md) — Set up a board on your server through the Coolify panel. +- [Deploy with Docker Compose](./getting-started/deployment/docker-compose.md) — Configure the containers, environment, and reverse proxy yourself. +- [Deploy on Vercel](./getting-started/deployment/vercel.md) — Set up managed services, migrations, and scheduled tasks. +### Moving an existing forum + +- [Migrate from MyBB or phpBB](./guides/migrating.md) — Rehearse an import, move the data, and check the result. +- [MyBB differences](./reference/mybb-parity.md) — Check feature and behaviour differences before migrating. +- [phpBB differences](./reference/phpbb-parity.md) — Check permissions, content, and account differences before migrating. + +## Getting started with Meith + +Learn the basics and prepare a new community for its first members. + +- [What is Meith?](./getting-started/introduction.md) — Understand boards, forums, threads, and the roles that run them. +- [Set up your community](./getting-started/first-steps.md) — Configure a new board and check it before inviting members. + +## Operating Meith + +Administer the community and maintain the services behind it. + +### Community administration + +- [Community administration](./guides/community/organiser-guide.md) — Manage forums, appearance, announcements, and members. +- [Forums and permissions](./guides/community/forums.md) — Arrange forums and control who can read and post. +- [Groups and promotions](./guides/community/groups.md) — Set group permissions, allowances, badges, and promotion rules. +- [Moderation](./guides/community/moderation-guide.md) — Approve posts, handle reports, and manage warnings and bans. +- [Spam controls and filters](./guides/community/antispam.md) — Configure registration challenges, rate limits, and content filters. +- [Search settings](./guides/community/search.md) — Configure search language, availability, and query limits. +- [Reputation settings](./guides/community/reputation.md) — Configure ratings, thanks, and who may give reputation. +- [Paid memberships with Dues](./guides/community/membership-guide.md) — Configure plans and manage payments, gifts, and the ledger. +### Server operations + +- [Server operations](./guides/operations/operating.md) — Check services and run routine maintenance commands. +- [Board configuration](./guides/configuration.md) — Understand the build files, runtime settings, and environment. +- [Install plugins and themes](./customization/installing.md) — Add, update, or remove an extension and deploy the change. +- [Authentication settings](./guides/operations/single-sign-on.md) — Configure two-factor authentication, sign-in providers, and passkeys. +- [Backups](./guides/operations/backups.md) — Schedule backups, copy them off-site, and test a restore. +- [Upgrade Meith](./guides/operations/upgrading.md) — Update the board and apply core and plugin migrations. +- [Disaster recovery](./guides/operations/disaster-recovery.md) — Restore the database, uploads, and secrets after losing a server. +- [Monitoring](./guides/operations/monitoring.md) — Check readiness, scheduler activity, logs, and metrics. +- [Scaling](./guides/operations/scaling.md) — Run multiple web instances with shared cache and storage. +- [Run a public demo](./guides/operations/demo-mode.md) — Deploy an isolated demo that resets its data on a schedule. + +## Using Meith + +Join a community, write posts, find discussions, and manage your account. + +- [Member guide](./guides/community/member-guide.md) — Register, take part in discussions, and manage your account. +- [Write and format posts](./guides/community/formatting.md) — Use Markdown, mentions, spoilers, code, and attachments. + +## Developing Meith + +Build themes and plugins, use the API, or contribute to Meith. + +### Build extensions + +- [Write your first plugin](./customization/first-plugin.md) — Scaffold, test, and run a plugin in a board. +- [Theme development](./customization/themes.md) — Implement theme slots, view models, tokens, and shared controls. +- [Plugin development](./customization/plugins.md) — Use hooks, UI regions, storage, routes, and scheduled tasks. +- [Translations](./guides/operations/internationalisation.md) — Add languages and translate themes and plugins. +- [Publish to the marketplace](./customization/marketplace.md) — Prepare a listing and understand the review requirements. +### API and reference + +- [REST API v1](./reference/api.md) — Endpoint, scope, and rate-limit reference. +- [Webhooks](./guides/operations/webhooks.md) — Subscribe to events and verify signed deliveries. +- [Theme slots and view models](./reference/theme-slots.md) — Generated reference for the theme contract. +- [Plugin hooks](./reference/plugin-hooks.md) — Generated reference for hooks, payloads, and call sites. +- [Performance](./reference/performance.md) — Recorded measurements and performance budgets. +### Contribute to Meith + +- [Contribute to Meith](./contributing/development.md) — Run the repository, make a change, and validate a pull request. +- [Architecture](./reference/architecture.md) — Understand the packages, processes, and request flow. +- [Next.js conventions](./contributing/nextjs-conventions.md) — Follow the repository’s routing, rendering, and action patterns. +- [Release procedure](./contributing/release.md) — Cut and publish a Meith release. + + -`pnpm verify` checks that generated references and both documentation indexes are current. When adding a document, add it to `apps/web/content/docs.manifest.json`, link it here, and run `pnpm site:docs`. +Documentation is maintained in `docs/` and published at [meith.dev/docs](https://www.meith.dev/docs). See [Development](./contributing/development.md#the-documentation-itself) for editing and regeneration instructions. diff --git a/docs/contributing/development.md b/docs/contributing/development.md index f8f3c20f1..f8546acc6 100644 --- a/docs/contributing/development.md +++ b/docs/contributing/development.md @@ -29,13 +29,17 @@ docker compose -f docker/compose.dev.yml up -d # Postgres on port 55432 cp .env.example .env ``` -Set two lines in `.env`: +Set these values in `.env`: ```sh DATA_SOURCE=postgres DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:55432/community_test +AUTH_SECRET= ``` +Generate the secret with `openssl rand -hex 32`. Keep it in `.env`, which is +ignored by git. The installer requires this secret even in development. + Then migrate and start: ```sh @@ -43,8 +47,8 @@ pnpm meith migrate pnpm dev ``` -Open and run the installer — the same one a -real deployment runs. It seals itself when it finishes; on a scratch +Open , unlock it with your `AUTH_SECRET`, +and run the installer. It seals itself when it finishes; on a scratch database that is fine, and `docker compose -f docker/compose.dev.yml down -v` gives you a clean one. @@ -825,26 +829,51 @@ pnpm api:docs # docs/reference/openapi.json, from the route registry pnpm perf:docs # docs/reference/performance.md, from the last load run ``` -`pnpm verify` fails when one is stale, deliberately: a reference read by -somebody who cannot see the source is worse than no reference when it is -wrong. +`pnpm verify` fails when a generated reference is stale. The theme generator +keeps property documentation in its description column and renders nested +property notes below the table. Type signatures omit embedded source comments; +the shared type formatter preserves member separators. ## The documentation itself -`docs/*.md` is the one editable copy. The site at +`docs/**/*.md` is the one editable copy. The site at [meith.dev/docs](https://www.meith.dev/docs) renders those same files at build time and holds no copy of any of them, so a correction is one edit in one place. +The manifest groups documents into **Setting up**, **Getting started with +Meith**, **Operating Meith**, **Using Meith**, and **Developing Meith**. +Keep member instructions in Using, community administration and server +maintenance in Operating, and extension contracts and contributor procedures +in Developing. Each section has one primary guide, marked **Start here** on +the index. The sidebar expands the current page's section; the index links +to every section. Search, the sitemap, and previous/next links use the same +manifest. Keep a document's published `slug` stable when changing its section +or source path. The site's quickstart links always target `quickstart`, +independently of its section. + +Keep introductions brief and lead with the task, prerequisites, and next +step. Link to the relevant reference instead of repeating deployment or +implementation details in every guide. Keep essential limits and recovery +steps; they are part of the procedure. + +When reviewing documentation against code, run the generated-reference and +link checks, then inspect the relevant commands, settings, routes, and tests. +Those checks prove registry and link consistency; they do not prove every +prose claim or third-party deployment step. Verify changing provider behaviour +against the provider's own documentation. + Adding a document means putting it in `docs/`, naming it in `apps/web/content/docs.manifest.json` — under `documents` to publish it, or -`internal` to keep it repository-only — linking it from -[`docs/README.md`](../README.md), and running: +`internal` to keep it repository-only — then running the generator to link it from +[`docs/README.md`](../README.md): ```sh -pnpm site:docs # rewrites the documentation table in the root README and checks the set +pnpm site:docs # rewrites the generated sections in docs/README.md and checks the set ``` +The root README stays a short introduction for newcomers and contributors. +It links to the documentation index and does not duplicate the catalog. Both index checks fail on a file that is in neither list, so a new document cannot quietly go unlinked. diff --git a/docs/contributing/nextjs-conventions.md b/docs/contributing/nextjs-conventions.md index 5bb2eb842..0d875258d 100644 --- a/docs/contributing/nextjs-conventions.md +++ b/docs/contributing/nextjs-conventions.md @@ -1,13 +1,8 @@ # Next.js conventions -The decisions that would otherwise be re-litigated in every pull request. -Link this from your PR description. - -> [!NOTE] -> Everything here is drawn from code that exists. The file paths are real, -> and the failure each rule prevents has actually happened in this -> repository. If you need to depart from a rule, say so in the PR -> description rather than quietly doing something else. +Follow these patterns when changing Meith's routes, components, actions, and +view models. Explain departures in the pull request. For local setup and +validation commands, see [Development](./development.md). ## The rules, in one table diff --git a/docs/customization/first-plugin.md b/docs/customization/first-plugin.md index 9cc49d5ed..7ee72b556 100644 --- a/docs/customization/first-plugin.md +++ b/docs/customization/first-plugin.md @@ -1,11 +1,8 @@ # Write your first plugin -This is the walkthrough: from an empty directory to a plugin running inside -a board and submitted to the marketplace, with a working extension at every -step. The policy — what a plugin may and may not do, and what the -guarantees cover — lives in [Plugins](./plugins.md); every hook and payload -is in the generated [Plugin hooks](../reference/plugin-hooks.md) reference. -This page assumes both exist and shows the path through them. +Scaffold a plugin, change a hook, test it, and run it in a board. You need +Node.js 22 or newer and npm. The [plugin API](./plugins.md) explains the +contracts; [Plugin hooks](../reference/plugin-hooks.md) lists the available hooks. ## Scaffold it diff --git a/docs/customization/installing.md b/docs/customization/installing.md index 972663f09..3f91d5e92 100644 --- a/docs/customization/installing.md +++ b/docs/customization/installing.md @@ -1,12 +1,8 @@ # Installing plugins and themes -A Meith board is a small code repository you own. Plugins and themes are npm -packages **built into that repository**, not uploaded into a running site — so -adding one is always the same three moves: install the package, register it, -redeploy. Nothing here needs to touch the server directly. - -If you would rather build your own than install one, start with -[Write your first plugin](./first-plugin.md). +Plugins and themes are built into your board. Install and register them in +the board repository, then commit and redeploy. The admin panel manages +the extensions already included in that build. ## Find one diff --git a/docs/customization/marketplace.md b/docs/customization/marketplace.md index cebecab11..4f670f205 100644 --- a/docs/customization/marketplace.md +++ b/docs/customization/marketplace.md @@ -1,20 +1,12 @@ -# The marketplace +# Publish to the marketplace -A curated, reviewed feed of the plugins and themes worth pointing a board -at — `marketplace/` in this repository, published at +The marketplace publishes extension metadata at [meith.dev/marketplace/v1.json](https://www.meith.dev/marketplace/v1.json) -and browsable, a page per listing, at -[meith.dev/marketplace](https://www.meith.dev/marketplace). - -It is metadata only. **Nothing is fetched through this feed.** Installing -a plugin or theme is still a package install, a line in -`meith.plugins.ts` or your theme selection, and a redeploy — the same -procedure described in [the plugin API](./plugins.md#writing-a-plugin) and -[the theme API](./themes.md), whose `pnpm add` is this repository's own -checkout speaking; a board of your own is a single package and installs -with `npm install` ([below](#moving-to-a-custom-board)). The feed exists so a board operator can -find out what is available and whether it is worth their trust before they -do any of that; it has no way to make the board do it for them. +and listings at [meith.dev/marketplace](https://www.meith.dev/marketplace). +Use this guide to prepare and submit a listing. + +The feed does not install code. Board operators should follow +[Installing plugins and themes](./installing.md). ## What is in a listing diff --git a/docs/customization/plugins.md b/docs/customization/plugins.md index ae740816e..c78844970 100644 --- a/docs/customization/plugins.md +++ b/docs/customization/plugins.md @@ -1,12 +1,9 @@ -# The plugin API +# Plugin development -`@meith/plugin-kit` is the contract between the board and a plugin. - -This document is the policy: what a plugin is, what it may and may not do, -and what the guarantees actually cover. The reference — every hook and every -payload — is generated into [Plugin hooks](../reference/plugin-hooks.md). To -**install** an existing plugin on a board you run, rather than write one, see -[Installing plugins and themes](./installing.md). +Use `@meith/plugin-kit` to add hooks, pages, UI regions, storage, and tasks. +Start with [Write your first plugin](./first-plugin.md) for a walkthrough; +use this guide and [Plugin hooks](../reference/plugin-hooks.md) for the contracts. +To add an existing plugin, use [Installing plugins and themes](./installing.md). ## Writing a plugin diff --git a/docs/customization/themes.md b/docs/customization/themes.md index 0da3bb711..56161ff3e 100644 --- a/docs/customization/themes.md +++ b/docs/customization/themes.md @@ -1,13 +1,9 @@ -# The theme API +# Theme development -`@meith/theme-kit` is the frozen contract between the board and a theme. - -This document is the policy: how to write a theme, what a theme may do, what -the freeze covers, and how something is removed from it. The reference — -every slot and every view model — is generated into -[Theme slots](../reference/theme-slots.md). To **install** an existing theme -on a board you run, rather than write one, see -[Installing plugins and themes](./installing.md). +Use `@meith/theme-kit` to implement a theme's slots, view models, and tokens. +This guide covers implementation and compatibility. The complete contract +is in [Theme slots and view models](../reference/theme-slots.md). +To add an existing theme, use [Installing plugins and themes](./installing.md). ## Writing a theme @@ -684,7 +680,9 @@ A sidebar or narrow panel therefore retains the compact arrangement even on a wide browser. Use the same named container when adding a listing, and keep its title and metadata usable in the compact layout. -The default board index includes a visible page heading, and the default +Bundled themes start the board index with its announcements and forum +listing, without a generic Community heading or introductory tagline. The +mark-all-read control remains available to signed-in members. The default shell shares a 1280px maximum width across its header, content, panels and footer. Its footer separates forum navigation from appearance preferences. Theme and scheme controls continue to submit native forms without scripting. diff --git a/docs/getting-started/deployment/coolify.md b/docs/getting-started/deployment/coolify.md index 244a540d1..9048e91f0 100644 --- a/docs/getting-started/deployment/coolify.md +++ b/docs/getting-started/deployment/coolify.md @@ -1,741 +1,281 @@ -# Deploying with Coolify - -You do not need to be a programmer to set up a Meith board. This page is -written for whichever volunteer drew the short straw: if you can rent a -server, point a domain at it, and follow along, it takes you from -nothing to a board the whole community can reach — on your own domain, -over HTTPS — in about twenty minutes. By default Coolify builds the image -itself, from your repository, so there is nothing to wait on before your -first deploy, and step 2 offers a route that needs nothing installed on -your own computer at all — not even a terminal. This guide walks that -default, **quick-start** path start to finish; an **advanced/prebuilt** -path exists too — GitHub builds the image ahead of time and Coolify only -ever pulls it, which trades a small amount of setup for a lighter server -build and a faster deploy — and this guide calls it out at each step where -the two diverge. - -This is the guided route, and the one most boards should take: -[Coolify](https://coolify.io) is a free panel you install on your server -once, and everything after it is a browser. It deploys a small board of -your own rather than this repository directly — the reason is -[the marketplace](../../customization/marketplace.md): a board built this way is yours from -the first deploy, so a plugin or theme worth installing later is a real -install, not a dead end. If whoever minds your machines would rather run -the compose file and a reverse proxy themselves, take -[Deploying by hand](./docker-compose.md) — same shape, more work. If you only -want to read the code or write a theme, [Development](../../contributing/development.md) -runs it on your laptop in two commands. - -And if the server is the part you would rather not have at all, there is a -fourth route: [Running on Vercel](./vercel.md) deploys a board of your own -onto functions, with a managed database, a managed cache and an object -store behind it instead of a machine. Nothing to patch, nothing to back up -by hand, no certificate to renew — at the cost of four bills rather than -one, a scheduler whose cadence its plan decides, and a board spread across -four companies rather than sitting on one box you rent. It is a real -option and deliberately not the default one, and that page is plain about -which parts of it are worse. Read it before picking it, particularly the -section on getting back off again. - -**You need:** - -| | | -|---|---| -| **A server** | Rented in the community's name, from any provider, for a few euro a month. 4 GB RAM, 2 vCPU, 40 GB disk is comfortable. Ubuntu 24.04 LTS below; any distro Docker runs on is fine. | -| **A domain** | With an `A` record already pointing at the server's IP — the certificate step needs it resolving. Your registrar's control panel does this. | -| **SSH** | Root, once, to install the panel. The terminal appears in step 1 and never again. | -| **A GitHub account** | Free. Your board's own repository lives there, and Coolify builds its image straight from it by default — no software of yours involved. Take the advanced/prebuilt path instead and GitHub's own runners build the image for you. | - -## 1. Install Coolify - -SSH into the server as root: +# Deploy with Coolify -```sh -curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash -``` +Deploy a board on your own server through the Coolify panel. This guide uses +a board repository created from the Meith template. Coolify builds it on the +server; a prebuilt-image option is included for servers that cannot build it. -It installs Docker if it is missing and serves its own UI on port -**8000**. +You need a server, a domain pointing at its IP address, and a GitHub account. +Keep the server and domain accounts accessible to the community's operators. +For other hosting options, see [Choose a deployment](./index.md). -Open `http://your-server-ip:8000` and **create the first account straight -away** — that registration page is open until somebody uses it. +## 1. Install Coolify -Then close the machine down to what is actually used: +Use a current Coolify release and follow its +[installation guide](https://coolify.io/docs/get-started/installation). +The official installer runs as root: ```sh -ufw default deny incoming -ufw allow OpenSSH -ufw allow 80,443/tcp -ufw allow 8000/tcp # the panel; drop this once it is behind a domain -ufw enable +curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash ``` -Coolify can serve its own UI over HTTPS on a subdomain of yours, using the -same proxy that will serve your board — worth doing before you close 8000. - -> [!NOTE] -> **Coolify v4.0.0-beta.411 or newer.** Magic environment variables in a -> compose file from a Git source arrived in that release, and they are -> what make this deploy ask you for nothing. The install script gives you -> the current version; an older existing install needs updating first. +Open the panel at `http://your-server-ip:8000` and create its first account. +Configure HTTPS for the panel and restrict access to the ports it needs. +The board will use ports 80 and 443 through Coolify's proxy. ## 2. Create your board -Three ways to get there — pick whichever matches what you have to hand. -All three end up in the same place: a repository on GitHub with the -deploy kit already in it. - -- **Rather not open a terminal at all?** Click **Use this template** on - [meith-dev/template](https://github.com/meith-dev/template) and name your - new repository — GitHub creates it and its first commit for you, no local - anything required, and there is nothing else to do in this step but its - last paragraph and the note under it. Skip the rest and read those: your - repository's first build has already started, and [step - 3](#3-set-your-domain-and-deploy) needs what it prints. -- **Have a terminal, nothing installed?** - - ```sh - curl -fsSL https://www.meith.dev/create-board.sh | bash -s -- my-board - ``` - -- **Already have Node.js and reach for `npx`?** - - ```sh - npx create-meith my-board - ``` - -If you used one of the two commands, pick `my-board`'s replacement now — -the name of the directory it writes and, once you push it, of the -repository on GitHub. (The template route asks for this itself, at the -point you click through.) It is not the board's display name (the -installer asks for that later, in [step -4](#4-run-the-installer)), so it does not have to be pretty, only lower-case -with no spaces. - -The two commands write an identical small workspace into `./my-board` — -`package.json`, `meith.config.ts`, and a deploy kit of its own carrying -**three** routes onto a server, not only the two this guide walks -through: `Dockerfile` and `docker-compose.yaml` for the default -quick-start path, `Dockerfile.prebuilt`, `docker-compose.prebuilt.yaml` -and `.github/workflows/build.yml` for the advanced/prebuilt one — the -same files the template repository already has — and -`docker-compose.byhand.yaml` for a third route with no panel at all; see -[Deploying by hand](./docker-compose.md) if that is the one you want -instead. Every one of the published-package files depends on `@meith/web` -and `@meith/cli` rather than containing a copy of this repository, and -this is what turns "installing a plugin" from a fork of this project into -`npm install` and a line in a config file — see [Consuming the board from -a workspace](../../contributing/development.md#consuming-the-board-from-a-workspace) -for the mechanism. Taking the quick-start path (the rest of this guide) -means the other files are never used — delete `Dockerfile.prebuilt`, -`docker-compose.prebuilt.yaml`, `.github/workflows/build.yml` and -`docker-compose.byhand.yaml` whenever you like, or leave them for later. - -**If you used curl or npx:** the command also initialized a git -repository in `./my-board` and staged every file, so the only commands -left are the ones only you can fill in — it prints them back to you, -ready to paste: +On [meith-dev/template](https://github.com/meith-dev/template), choose +**Use this template** and create your board repository. This is the simplest +route if you do not already have a local checkout. + +Alternatively, create it locally with Node.js 22 or newer: ```sh +npx create-meith my-board cd my-board -git commit -m "Scaffold my-board" +git commit -m "chore(board): scaffold my-board" git remote add origin https://github.com//my-board.git git push -u origin main ``` -Create that empty repository on GitHub first — **New repository**, no -README, no `.gitignore` — then run the four lines above. (Skip this whole -part if you used the template — GitHub already did it.) - -On the quick-start path nothing else happens here — step 3 points Coolify -straight at your repository and it builds `Dockerfile` itself, on every -deploy. **Taking the advanced/prebuilt path instead:** -`.github/workflows/build.yml`, already written, builds `Dockerfile.prebuilt` -on GitHub's own runners the moment `main` has something pushed to it — for -the template route, that means the moment GitHub finishes creating your -repository — and pushes the result to `ghcr.io//my-board`, using -only the `GITHUB_TOKEN` every Actions run already carries. No Docker on -your computer, no registry account beyond the GitHub account you already -have. - -> [!IMPORTANT] -> **Advanced/prebuilt path only — before moving on, wait for that build.** -> Step 3's advanced variant asks you for a value only the finished run -> knows, so there is no starting it early. Open your repository's -> **Actions** tab — **Build and push** is already running, or already -> done — and once it is green: -> -> 1. Open the run and read its **Summary**. -> 2. Copy the image value it shows — you'll paste it into Coolify in -> [step 3](#3-set-your-domain-and-deploy). -> 3. Follow the Summary's link to the package itself and check it says -> **Public**. Often it already does — a build from a public repository -> usually publishes a public package — in which case there is nothing -> to do. If it says Private, change it here (**Package settings** → -> **Change visibility** → **Public**) while you are on the page: -> Coolify cannot pull a private package, and fails the deploy with an -> authentication error rather than an explanation. +Create the empty GitHub repository before adding the remote. The directory +name is separate from the display name you will choose in the installer. + +The deployment files are: + +| Files | Purpose | +|---|---| +| `Dockerfile`, `docker-compose.yaml` | Build and deploy through Coolify | +| `Dockerfile.prebuilt`, `docker-compose.prebuilt.yaml`, `.github/workflows/build.yml` | Build on GitHub Actions and deploy the resulting image | +| `docker-compose.byhand.yaml` | [Deploy without Coolify](./docker-compose.md) | + +For the prebuilt option, wait for **Build and push** in the repository's +**Actions** tab. Its summary gives the image name. Make the GHCR package +public, or configure registry credentials in Coolify so it can pull it. ## 3. Set your domain and deploy -In the panel: **New Resource → Public Git repository**. Paste your -repository's address into **Repository URL** and press **Check -repository** — Coolify reads the repository and opens **Build -configuration** underneath, where one field needs changing and the rest -are already right: +In Coolify, choose **New Resource → Public Git repository**, enter your +repository URL, and check the repository. Use: | Field | Value | |---|---| -| Repository URL | `https://github.com//my-board` | | Branch | `main` | -| Build pack | **Docker Compose** — the one field on this screen you have to change | +| Build pack | Docker Compose | | Base directory | `/` | -| Compose file | `/docker-compose.yaml` — already correct; your repository's file is named for this default | - -Then press **Continue**, which creates the resource. - -Coolify offers a generated domain and accepts your own. Put yours in — the -one whose `A` record points at this server. - -There is nothing else to set: `docker-compose.yaml` builds `web` and -`migrate` from `Dockerfile` in your repository itself, so there is no image -value to look up or paste in. Skip straight to deploying, below. - -> [!NOTE] -> **Taking the advanced/prebuilt path instead?** Change **Compose file** to -> `/docker-compose.prebuilt.yaml` before you press **Continue**, then, once -> the resource exists, open its **Environment Variables**, add -> `MEITH_IMAGE`, and give it the `:latest` value step 2's Actions run -> Summary printed — `ghcr.io//my-board:latest`. That compose file -> refuses to start without it. `:latest` follows your repository's `main` -> branch: installing a plugin later is a push and a **Redeploy**, with -> nothing on this screen to edit — the right trade while the board is young -> and you are still changing it. The Summary prints a second value beside -> it, the same image ending in a long commit code, which names that one -> build and nothing else, ever. Move `MEITH_IMAGE` to that once the board -> is settled and you want upgrades happening only when you choose, not on -> any redeploy. - -Now deploy: the button is on the resource's own **Actions**, and it is the -step everything above was setting up, so leave it until the domain is in -(and, on the advanced path, `MEITH_IMAGE` too). The quick-start path builds -the image on the server, which takes longer than a pull and can strain a -2 GB box; the advanced path only ever pulls one. Either way, four -containers come up, in order: - -| Container | What it does | +| Compose file | `/docker-compose.yaml` | + +Continue, set the board's domain, then deploy. Its DNS record must point at +the server for the proxy to issue a certificate. + +For a **prebuilt image**, use `/docker-compose.prebuilt.yaml` instead and set +`MEITH_IMAGE` in the resource's environment to the image printed by the build +workflow. A commit-specific tag selects one build; `:latest` follows the most +recent build of `main` and may change on a redeploy. + +The stack contains: + +| Service | Expected state | |---|---| -| `postgres` | The database. A named volume, so recreating the container keeps the data. | -| `migrate` | Applies the schema and **exits 0**. The next two wait for it, so the code never runs against a schema behind it. | -| `web` | The board itself. Uploaded files — avatars, board images, attachments — live in a named `uploads` volume, so a redeploy keeps them; point `FILESTORE_DRIVER` at S3 or a Blob store instead if you would rather they live outside the server. | -| `worker` | Not the compiled tick process — `@meith/worker` is not published, so a board built this way calls `/api/system/tick` from a small loop instead. The catch-up it triggers still runs inside `web`, so `web`'s log, not this container's, is where a long `ran` list shows it working. This container only logs on failure. | - -Four things happen without your involvement, and they are the reason this -route exists: - -- **The secrets are generated.** `AUTH_SECRET` and `TICK_SECRET` come from - Coolify's magic variables, filled in on the first deploy and kept for - the life of the resource; so does the database password. All three are - visible in the panel; none is typed. -- **The board is told its own URL** (`APP_URL`), which every link in an - outgoing e-mail is built against. -- **The certificate is issued and renewed** by Coolify's proxy. -- **Nothing is published on the host**, so the proxy is the only way in. - -The same **Environment Variables** tab tunes the containers' resource -ceilings, which default to a small VPS: `WEB_MEM_LIMIT`, `WEB_CPUS`, -`POSTGRES_MEM_LIMIT`, `POSTGRES_CPUS`, `WORKER_MEM_LIMIT` and -`WORKER_CPUS` override the compose file's defaults the same way -`MEITH_IMAGE` does on the advanced path, so a larger server is a variable -on the resource, never an edit to the file. - -> [!NOTE] -> **Redeploys, and deploying without a gap.** On the quick-start path, -> every **Redeploy** rebuilds `web` and `migrate` from your repository's -> current `main`, so a newly installed plugin is picked up without an -> image to update anywhere. On the advanced path, those two services set -> `pull_policy: always` instead, so every **Redeploy** fetches the current -> image for the tag rather than reusing a `:latest` the host already has — -> a rebuild on `main` is picked up without pinning a new digest. Either -> way, Coolify recreates a compose stack by default: it stops the old -> containers before starting the new ones, so the board is briefly down while -> `web` boots. To close that gap, turn on **Rolling update** for the resource -> (its **General** settings). `web` declares a `/api/ready` healthcheck for -> exactly this — with rolling enabled Coolify waits for the new container to -> pass it before retiring the old one, so the swap carries no downtime. +| `postgres` | Running with board data in the `pgdata` volume | +| `migrate` | Exited with code 0 after applying core migrations | +| `web` | Running, with uploaded files and backups in persistent volumes | +| `worker` | Running a loop that calls the web service's tick endpoint | + +The scaffold's worker is an HTTP caller. Scheduled work executes inside +`web`, so check the web logs for task results; the worker logs failed calls. + +Coolify generates the database password, `AUTH_SECRET`, and `TICK_SECRET` +and supplies the board's `APP_URL`. Check that the URL is your public HTTPS +origin. Save the generated secrets as described in step 6. + +The Compose file exposes no host ports. Its resource limits can be changed +through `WEB_MEM_LIMIT`, `WEB_CPUS`, `POSTGRES_MEM_LIMIT`, `POSTGRES_CPUS`, +`WORKER_MEM_LIMIT`, and `WORKER_CPUS`. + +**Plan for a brief interruption on redeploy.** Coolify does not support +[rolling updates for Docker Compose deployments](https://coolify.io/docs/knowledge-base/rolling-updates). +The readiness healthcheck helps detect a failed start; it does not make this +Compose deployment a rolling deployment. ## 4. Run the installer Open `https://your-domain/install`. -It checks your environment **before** offering a form, and separates two -kinds of problem: - -| It says | It means | -|---|---| -| **Blocker** | Installing cannot succeed — a missing variable, an unreachable database. There is no form until it is fixed. | -| **Warning** | Installing will succeed and something will be wrong *later*. | - -Everything that passed is folded into an "*N* checks passed" line you can -expand. What is left on the page is what needs a decision — so read the -warnings: nearly every way a new board disappoints somebody a month in is -visible on that screen on day one. - -The form is three numbered sections — **Your board**, **Your account** and -**Sending mail**. The first two are four boxes between them: what the -board is called, and the name, address and password of your account. It -does not ask for the board's address on this route — Coolify supplies it, -so the installer shows it as already decided. - -The third section is a **list of mail providers**, not a page of server -details: pick the one you already have and it fills in the host, port and -TLS mode, leaving you a sender address and one credential to paste. -[Step 5](#5-mail) is the answer sheet for that list — and mail is the one -thing on this form that is harder to add later than now. - -> [!NOTE] -> **Your username is the name you post under**, not a role. `admin` and -> `administrator` are reserved — along with `root`, `moderator`, `mod`, -> `staff`, `system`, `guest`, `anonymous`, `me` and `you` — so no account -> can impersonate the board. The form lists them under the box. Being an -> administrator is a group membership, and your account is put in it -> either way. - -Pressing Install runs five steps, listed beside the button under **What -installing does**: - -1. **Check the schema** — confirms every table the board needs is already - there, and stops with the names of any that are not. It does not - migrate: the container entrypoint and the Vercel build command both do - that before the board serves anything, so by the time you reach this - form the work is done. If it does report missing tables, run - `meith migrate` against the same database and reload. -2. **Record the board's name and mail settings** — the only settings it - writes. -3. **Create the administrator** — your account. -4. **Create a first forum** — so the index is not empty. -5. **Disable the installer.** - -If you filled in the mail section, a **test message goes to your address -before step 1**, and nothing is installed if it fails. A mistyped API key -costs you a retry on this form rather than a finished board that cannot -e-mail anybody. - -> [!CAUTION] -> Step 5 is irreversible: `/install` answers 404 from then on, on -> purpose. You are running this against the production database, which is -> the right place — just do not run it twice against two different ones. - -Running it twice against the *same* one is safe, including at the same -moment from two browsers. The installer takes a lock on the database -before it does anything and re-checks the seal inside it: the second -attempt is told an install is already running, or — if the first has -finished — sent to the finished board. It does not migrate a second time, -and it does not create a second administrator. - -That is a board. It sends you to the sign-in page; sign in with the -account you just made. - -If the header still says *Meith* rather than your board's name, wait a -minute and reload — settings are cached briefly, and the name you typed -outlives the cache. - -Then go to **`/admin`**, which asks for your password a second time. That -is not a bug: the control panel keeps a session of its own, separate from -your board session, so an unattended browser that is signed in to the -board is not also signed in to the panel. It lapses after 30 minutes -idle, and after 8 hours regardless. +1. Resolve any preflight **blockers**. Review **warnings** before proceeding. +2. Unlock the installer with the deployment's **`AUTH_SECRET`**. In Coolify, + this is the value of `SERVICE_BASE64_64_AUTH` in the resource's environment. + The unlock lasts 30 minutes in that browser and is also required for restore. +3. Enter the board's name and your administrator account details. Choose your + own posting name; role names such as `admin` and `moderator` are reserved. +4. Configure mail using step 5. If you provide mail settings, the installer + sends a test to your address before it writes the installation data. +5. Select **Install**. + +The installer checks the schema, saves the initial settings, creates your +administrator account and a first forum, then seals itself. It does not +apply migrations: the `migrate` service must already have completed. After +installation, `/install` returns 404. + +Sign in with the new account. Opening `/admin` asks for your password again +because the admin panel has its own session. Continue with +[Set up your community](../first-steps.md) after completing the backup checks. ## 5. Mail -**This is the answer sheet for section 3 of the installer**, so read it -before filling that section in. If the board is already installed, the -same settings live at **`/admin/settings?group=mail`** and take effect on -the next message — no redeploy either way. - -> [!IMPORTANT] -> A board with no mail configured **sends nothing at all**. Each message -> is written to the container log and stops there. Password reset fails -> silently, and if registration asks for a confirmation link, nobody can -> finish signing up. Nobody notices until the first member cannot get -> back in. +Configure mail in the installer, or later at `/admin/settings?group=mail`. +Without a sending provider, messages go to the container log and are not +delivered. Password reset and email activation need working mail. ### Pick the provider you already have -**How mail is sent** is a list that opens on *Skip for now — this board -sends no mail*, which is a real answer and the wrong one for most boards. -Every other row is the ordinary SMTP or API transport with the fiddly -half typed in for you — prefills rather than integrations, so anything -you type yourself wins over the preset: - -| Choose | It already knows | You give it | -|---|---|---| -| **A mailbox I already have (SMTP)** | Port 465, implicit TLS | Sender address, your provider's SMTP host, your mailbox address as the username, and an app password — never the password you sign in with | -| **Resend (API)** | The endpoint | Sender address and the API key | -| **Resend (SMTP)** | `smtp.resend.com`, 465, implicit TLS, username `resend` | Sender address, and the API key as the password | -| **Brevo (SMTP)** | `smtp-relay.brevo.com`, 587, STARTTLS | Sender address, and Brevo's SMTP login and key | -| **Postmark (SMTP)** | `smtp.postmarkapp.com`, 587, STARTTLS | Sender address, and the server API token as **both** username and password | -| **Amazon SES (SMTP)** | Port 587, STARTTLS | Sender address, `email-smtp..amazonaws.com`, and SMTP credentials — *not* your AWS access keys | -| **Any other SMTP server** | Port 587, STARTTLS | Sender address, the host, and credentials if the server wants them | -| **Any other JSON API** | Nothing | Sender address, endpoint and key — works only if the provider takes Resend's exact field names | - -Three boxes appear whichever you pick — **Sender address**, **Username** -and **Password or API key** — plus **Server details**, for the rows that -still need a hostname. A box left blank uses the preset's own value. - -**If you receive mail on this domain already** — Fastmail, Migadu, Google -Workspace, your host's mailbox — take the first row. It is the shortest -path by a distance, because SPF and DKIM are already published for the -domain and there are **no DNS records to add**. - -**Everything else on the list needs the sending domain verified with the -provider first**, and the board cannot do that step for you. Until it is -done, a new account can usually only mail the address you signed up -with — and SES additionally starts in a sandbox that needs a support -request to leave. The installer says which caveat belongs to which -provider; free tiers and deliverability are compared in -[Operations § Mail](../../guides/operations/operating.md#mail). +The installer offers presets for common providers: -### The installer proves it before writing anything +| Provider | Information you supply | +|---|---| +| Existing mailbox or another SMTP server | SMTP host, sender address, and the provider's sending credentials | +| Resend API | Sender address and API key | +| Resend SMTP | Sender address and API key as the password | +| Brevo SMTP | Sender address, SMTP login, and SMTP key | +| Postmark SMTP | Sender address and Server API token as username and password | +| Amazon SES SMTP | Sender address, regional endpoint, and SMTP credentials | +| Another JSON API | Sender address, endpoint, and token; it must accept the HTTP driver's message format | + +Use the provider's instructions for domain verification and sending +credentials. An existing mailbox does not guarantee that every sender +address on its domain is authorised. -Press Install and a real message goes to your address **before the first -migration**, with nothing installed if it fails. A provider that refuses -says why, and that sentence is put on the form word for word — "the -domain example.com is not verified" is the whole answer. +### The installer proves it before writing anything -That is what makes this a minute now rather than a visit later: a wrong -key found here costs a retry; the same key found afterwards costs a -sealed board that cannot e-mail anybody, fixable only from a panel you -have not seen yet. +When you configure mail during installation, Meith sends a test before +writing the board settings or creating the administrator. A failed test +leaves the form available for correction. The schema was already migrated +during deployment. ### If you skipped it -Configure it at **`/admin/settings?group=mail`**. Same settings, minus -the provider list — that screen is generated from the settings registry, -so **How mail is sent** there is the transport (*SMTP server*, or -*Provider API*) and you type the host, port and security mode from the -table above. - -**Save**, then press **Send a test message to me**. It sends through what -is *stored* — so save first — and shows the provider's refusal verbatim. - -Finally, check **Activation method** under -`/admin/settings?group=registration`: it decides whether new members need -a confirmation link at all, and it is the one setting that turns a mail -problem into a board nobody can join. +At `/admin/settings?group=mail`, choose the transport, enter the settings, +and **Save**. Then choose **Send a test message to me** and confirm delivery. +The test uses the saved settings. -> None of this is an environment variable, and on this route none of it -> needs to be. `MAIL_DRIVER` and its companions still exist and still win -> outright when set — for deployments configured wholly from files, at -> the cost of a redeploy to rotate a key. See -> [Operations § Mail](../../guides/operations/operating.md#mail). +Check **Activation method** at `/admin/settings?group=registration` before +opening registrations. Do not require email confirmation until mail works. +Environment `MAIL_*` values override the corresponding panel configuration; +see [Mail](../../guides/operations/operating.md#mail) if a saved change is not used. ## Installing a plugin or theme -Nothing installs into a running container — a plugin or theme has to be -built into the image, the same as `npm install` anything else. In the -board repository: - -1. Add it. A **plugin** is one command — it installs the package and - registers it: `npm run meith -- plugin:add `, e.g. - `@meith/plugin-dues`. A **theme** is - `npm install --save-exact ` (e.g. `@meith/theme-midnight`), then - an entry in `meith.config.ts`'s `themes` map following the shape of the - `default` one there, with `defaultTheme` set to its key to make it the - board's default. -2. `git commit` and `git push`, then press **Redeploy** in Coolify. - Pushing alone does not rebuild: quick-start builds the new image on - that redeploy; advanced/prebuilt waits for `.github/workflows/build.yml` - to finish first, and Redeploy is what actually pulls the result. -3. If it ships database changes, apply them once it is up from - **Admin → System** (**Version & migrations**), or - `docker compose run --rm web meith upgrade`. - -[Installing plugins and themes](../../customization/installing.md) is the -full guide, and [Plugins](../../customization/plugins.md) and -[Themes](../../customization/themes.md) are the authoring references. +Follow [Installing plugins and themes](../../customization/installing.md) +in your board checkout. Commit and push the change, then redeploy. With the +prebuilt option, wait for the image build to finish before redeploying. -## Running commands (the CLI) without SSH +Apply pending plugin migrations through **Admin → System → Version & migrations** +or `meith upgrade`. Core migrations run in the deployment's `migrate` service. -Most day-to-day maintenance is in the browser admin panel — settings, users, -forums, and, under **Admin → System**, the search reindex, recount, cache -clearing and **applying a release's migrations**. What is left is the `meith` -operator CLI, and Coolify runs it without a shell of your own. +## Running commands (the CLI) without SSH -**A one-off command.** Open the board resource's **Terminal** in Coolify, -choose the `web` container, and run `meith ` directly: +Open the resource's **Terminal**, choose the `web` container, and run: ```sh meith env:check meith settings:get board.name -meith upgrade +meith --help ``` -`meith` is on the image's `PATH`, so there is no path to type and nothing to -install first. (From a plain host shell on the server it is -`docker compose exec web meith ` instead — the Terminal saves you -that.) - -These are **runtime** commands — they act on the running board and its -database. Installing a plugin or theme is different: it edits your board's -repository and only takes effect on a rebuild, so it is done in your checkout, -not here — `plugin:add` refuses in the container for that reason. See -[Installing plugins and themes](../../customization/installing.md). +These commands act on the deployed board. `plugin:add` belongs in the board +checkout because it edits files that must be rebuilt; it refuses to run in +the deployed container. -**Something recurring.** Add a **Scheduled Task** to the resource — a name, the -command, the container (`web`), and a cron schedule — and Coolify runs it in the -container on that schedule, with a button to run it now. Backups no longer -need one (see below), but a report you script yourself might. - -The [operator CLI reference](../../guides/operations/operating.md#the-operator-cli) -lists every command; `meith --help` inside the container lists what your -installed release actually has. +**Admin → System** also offers search reindexing, recounting, cache clearing, +and pending plugin migrations. See [Server operations](../../guides/operations/operating.md) +for command details and account recovery. ## 6. Set up backups -Not optional, and mostly not the panel's job either: Coolify's own -per-resource backup schedule dumps the database and does **not** include -the uploads volume — avatars, attachments, board images — and finding that -out during a restore is the worst possible time. The board takes its own -backups, database and uploads together, and the compose file already -mounts a named `backups` volume at `/backups` for them to land in, so the -bundles survive every redeploy. Three steps, and the first one is not a -schedule at all. +Use Meith's backup system to include both the database and uploaded files. +A database-only backup does not contain attachments, avatars, or board images. ### First, copy the generated secrets off this server -Coolify generated three values on the first deploy and holds them nowhere -but this machine: `SERVICE_BASE64_64_AUTH` (the board's `AUTH_SECRET`), -`SERVICE_BASE64_64_TICK` (`TICK_SECRET`) and `SERVICE_PASSWORD_POSTGRES` -(the database password). Open the resource's **Environment Variables**, -and put all three in the community's password manager now, before the -first nightly backup ever runs. No backup schedule replaces this step: -the bundles deliberately do not contain the secrets, and losing -`AUTH_SECRET` with the server strands every member's authenticator-app -enrolment even after a perfect restore — -[Disaster recovery](../../guides/operations/disaster-recovery.md#what-recovery-consumes) -prices each of the three. - -### Turn on the schedule - -On the board, **Admin → Settings → Backups**: set **Automatic backups** -to *Every day*, leave the time at 02:00 UTC or pick the board's quietest -hour, keep the retention at 7, and save. That is a timestamped bundle — -database dump and uploads together — into the `backups` volume every -night, keeping the newest **7**, a week of nightly restore points. The -`worker` container takes it, in a lane of its own, so the board keeps -serving and mail keeps leaving while the dump runs. - -Then **Admin → System → Backups** and **Back up now**, today rather than -at 02:00: the bundle appears in the list within a couple of minutes, with -its size, and **Recent runs** records the run. A run that fails shows -there in red, on the System screen as a failing task, and as a -notification to every administrator — which beats discovering it the day -you need the bundle. - -Each bundle carries every upload, so seven bundles is roughly seven times -the board's data; **Backups to keep** is the knob if the disk gets tight. -A run that skips objects it cannot read is recorded as *done, incomplete* -— [worth understanding](../../guides/operations/backups.md#when-a-bundle-is-incomplete), -not worth discarding. - -### Then ship the bundles off the server - -The ring on `/backups` shares a disk with the board, so it protects -against a bad upgrade or a deleted forum — not against losing the server. -Name a bucket and every backup also ships its bundle there, pruned to the -same retention: - -1. Create a bucket at any S3-compatible provider — Backblaze B2, - Cloudflare R2, Hetzner, Scaleway, MinIO on a machine you trust — a few - euro a month at forum size. **A bucket of its own**: never the bucket - uploads live in, if you moved those to S3. Give its credential write, - list and delete on that bucket only. -2. Either pick the bucket as the **Off-site destination** under - **Admin → Settings → Backups** and fill in its fields — the secret is - stored sealed under the board's `AUTH_SECRET` — or, when the - credential must not live in the database, set `BACKUP_S3_BUCKET`, `BACKUP_S3_REGION`, - `BACKUP_S3_ACCESS_KEY_ID` and `BACKUP_S3_SECRET_ACCESS_KEY` on the - resource's **Environment Variables** — plus `BACKUP_S3_ENDPOINT` for - anything that is not AWS itself (with `BACKUP_S3_REGION=auto` for R2), - and `BACKUP_S3_PREFIX` if one bucket serves several boards — and - **Redeploy**. All four required values or none; the environment wins - when both are set. The environment route is also the one a fresh - resource can use to find its bundles before it has any settings, which - is what [Disaster recovery](../../guides/operations/disaster-recovery.md#under-coolify) - leans on. -3. Prove the shipping happened: **Test the destination** on the Backups - screen lists the bucket, and **Back up now** puts the first bundle in - it — the list then shows the bundle as both *on the server* and - *off-site*. An upload nobody has listed is a hope, not an off-site - copy. - -A Nextcloud or a Hetzner Storage Box works in place of the bucket: pick -**A WebDAV folder** as the destination instead, and give it the folder's -address and an app password. -[Backups](../../guides/operations/backups.md) is the full reference — -the settings, both kinds of destination, the `meith backup` command for a -Scheduled Task you would rather own, and the restore. +Save these values from Coolify's environment in the community's password manager: -## 7. Prove the restore +- `SERVICE_BASE64_64_AUTH` — the board's `AUTH_SECRET`. +- `SERVICE_BASE64_64_TICK` — the board's `TICK_SECRET`. +- `SERVICE_PASSWORD_POSTGRES` — the database password. -A backup nobody has restored is a file, not a backup. This rehearsal -takes ten minutes in the panel's Terminal, touches nothing the live board -uses, and turns your bundles from files into backups. Do it once now, and -again whenever the deployment changes shape. +Backup bundles do not include these environment secrets. In particular, +restoring without the original `AUTH_SECRET` leaves sealed credentials and +authenticator enrolments unreadable. -1. **Terminal → `postgres`**: create a scratch database beside the real - one. +### Turn on the schedule - ```sh - createdb -U community rehearsal - ``` +1. Open **Admin → Settings → Backups**. +2. Set **Automatic backups** to daily, choose a quiet time in UTC, and choose + how many bundles to keep. Seven daily bundles keep a week's restore points. +3. Save, then open **Admin → System → Backups** and choose **Back up now**. +4. Check the bundle and its result under **Recent runs**. -2. **Terminal → `web`**: pick a bundle and restore it into the scratch - database. `meith restore` refuses to run without an explicit - `RESTORE_DATABASE_URL` and refuses any database that is not empty, so - it cannot be aimed at the live board by accident; the substitution - below reuses the connection string the board already has, swapping the - database name on the end. +The Compose file mounts `/backups` as a persistent volume. Every bundle +includes uploads, so allow enough disk space for the chosen retention. +Scheduled backup work runs through the HTTP tick inside `web` on this route. - ```sh - meith backup:list --dir /backups - RESTORE_DATABASE_URL="${DATABASE_URL%community}rehearsal" \ - meith restore /backups/ --skip-uploads - ``` +### Then ship the bundles off the server - Read what it prints: the backup's date and version, migrations (on a - fresh bundle: nothing to do), and **the restored post count** — the - number that tells you the bundle is real. `--skip-uploads` keeps the - rehearsal off the live uploads directory; the bundle's uploads half is - validated as part of reading the bundle either way. +Choose an S3-compatible bucket or WebDAV folder under **Admin → Settings → +Backups**, enter its details, and use **Test the destination**. Run another +backup and verify that it appears off-site. -3. **Terminal → `postgres`**: drop the evidence. +For credentials supplied through the deployment, use the `BACKUP_S3_*` or +`BACKUP_WEBDAV_*` variables instead and redeploy. Keep the backup destination +separate from the upload store. See [Backups](../../guides/operations/backups.md) +for the required fields, retention, and incomplete-bundle handling. - ```sh - dropdb -U community rehearsal - ``` +## 7. Prove the restore -Note the date and the post count somewhere that is not this server. If -step 2 failed, today is the cheap day to find out why. +Test on a separate database and uploads directory before relying on a backup. +Follow [Disaster recovery — rehearsal](../../guides/operations/disaster-recovery.md#rehearse-it-and-write-the-number-down) +for the procedure. Verify that a restored account can sign in, a thread +opens, and an attachment downloads. Record the result somewhere off the server. ## Restoring for real -Two situations, one rule for both: a restore only ever writes into an -empty board, so the restore is always *replace, then verify* — never -patch in place. - -**Rolling the board back** — a bad upgrade, a plugin gone wrong, a -mistake that deleted real content. Warn the members if you can; the board -is down from the drop until the restart. - -1. **Terminal → `postgres`** — drop the live database and recreate it - empty. `with (force)` disconnects the running board, which errors - until the restart below and no further: - - ```sh - psql -U community -d postgres -c 'drop database community with (force)' - createdb -U community community - ``` - -2. **Terminal → `web`** — for a full restore including uploads, empty the - uploads volume first (the restore insists on a fresh directory for the - same reason it insists on an empty database), then restore. When the - uploads are fine — a database-only incident — keep them and add - `--skip-uploads` instead: - - ```sh - find /app/.uploads -mindepth 1 -delete - RESTORE_DATABASE_URL="$DATABASE_URL" meith restore /backups/ - ``` - - Restoring a bundle older than the deployed release applies the - migrations in between itself; restoring after a bad upgrade should - instead go back to the release the bundle was taken from — pin - `MEITH_IMAGE` accordingly, and read - [Downgrades](../../guides/operations/upgrading.md#downgrades) before - deciding which. - -3. **Restart** the resource, then verify before announcing anything: - sign in, open a thread with attachments, check - `/admin/settings?group=mail` still sends. (On this compose resource - the panel's Restart re-runs the deployment from your branch's head — - the [redeploys note](#3-set-your-domain-and-deploy) explains when that - also moves the version.) - -**The server is gone** — follow -[Disaster recovery](../../guides/operations/disaster-recovery.md), which -has the order of operations and the verification list; its -[Under Coolify](../../guides/operations/disaster-recovery.md#under-coolify) -section maps each step onto a fresh panel, including the one trap worth -knowing in advance: paste your saved secrets over the newly generated -ones **before** the first deploy. On a fresh resource the installer -itself offers the bundles in the bucket, so the restore is a page, not -a terminal. +Restore to a fresh deployment using [Disaster recovery](../../guides/operations/disaster-recovery.md#under-coolify). +Set the saved secrets before the first deploy, unlock `/install` with +`AUTH_SECRET`, and choose a backup. Do not run the new-board installation +form on a destination you intend to restore into. + +For rollback after an upgrade, also read +[Downgrades](../../guides/operations/upgrading.md#downgrades). Reverting the +image does not undo database migrations. ## If the install fails halfway -The run stops at the first failed step, and the step list beside the -button reopens as **How far it got** — each step marked *done*, *failed* -or *not run*. That list is the answer to "is it safe to press this -again", so read it before you do. - -Most of what stops an install is an **answer**, not a fault — a reserved -username, a password below the form's minimum — and those are refused on -the form itself, with the message beside the box that caused it, before -any step runs. Change the answer, retype the passwords, and press Install -again. - -Sealing is deliberately last, so a failure before it leaves a board you -can fix and retry: - -- **It failed before the administrator was created.** Fix the cause and - run it again — migrations and the settings step are both safe to apply - twice. -- **It failed after the administrator was created.** The installer - refuses to run again: its preflight blocks on *any* account existing, - so a retry cannot add a second administrator to a board that already - has members. If the board is genuinely yours to reset, recover at the - database — restore the empty database, or drop and recreate it — and - start again. If the only thing missing is administrator access on a - board that otherwise works, do not reinstall: use the - [operator CLI](../../guides/operations/operating.md#account-recovery) — - `meith user:promote`. +Read the step report. If no administrator was created, correct the failure +and retry. If an account already exists, preflight blocks another installation; +use [Account recovery](../../guides/operations/operating.md#account-recovery) +for a working board whose administrator cannot sign in. + +Do not reset a database that already holds community content to rerun the installer. ## When something else goes wrong -| What you see | What it is | +| Symptom | Check | |---|---| -| The deploy fails before any container starts, complaining that `MEITH_IMAGE` is unset | Advanced/prebuilt path only: you skipped setting it before deploying, or it is set on the wrong resource — [step 3](#3-set-your-domain-and-deploy). That compose file will not guess an image for you. | -| The deploy fails pulling the image, with an authentication error | Advanced/prebuilt path only: the GHCR package is private — [step 2](#2-create-your-board)'s note on checking its visibility. | -| The build fails on the server, or takes a very long time | Quick-start path: `Dockerfile` installs this board's full dependency closure on the box itself, which can OOM a 2 GB VPS. Move to the advanced/prebuilt path — [step 3](#3-set-your-domain-and-deploy) — and let GitHub's runners do the heavy lifting instead. | -| `migrate` exits non-zero | Read its log. A failed migration stops the stack on purpose rather than serving against a half-applied schema. | -| The `worker` container logs `tick failed` repeatedly | The board it is calling is not answering — check `web`'s own log first; the loop container has no logic of its own to break. | -| 413 on an upload | The proxy's body limit, not the board's. Raise it on the resource. | -| Password reset "sent" and never arrives | Mail is not configured, so the message is sitting in the web container's log. Check `/admin/settings?group=mail` and press the test button. | -| Nothing happens on a schedule | The `worker` container is not running, so nothing is calling `/api/system/tick` — see `/admin/system`. | -| The board is on a newer version than you deployed | Quick-start path: every **Redeploy** builds whatever is on `main` right now, working as intended — a push since your last deploy, adding a plugin say, is what the next Redeploy picks up. Advanced/prebuilt path: `MEITH_IMAGE` is on the `:latest` tag step 3 sets, working as intended the same way; move it to the commit-sha value instead — [step 3](#3-set-your-domain-and-deploy) — if you would rather that never happen unasked, since neither button can do it on its own. | - -[Operations § Troubleshooting](../../guides/operations/operating.md#troubleshooting) -covers the failures that are about the board rather than the deploy. +| Prebuilt deployment says `MEITH_IMAGE` is missing | Set it on the resource using the image build's summary | +| Image pull is denied | GHCR package visibility or Coolify's registry credentials | +| Build runs out of memory | Build elsewhere and use the prebuilt option | +| `migrate` exits non-zero | Its log; `web` waits for successful core migration | +| Installer asks for a secret | Enter `SERVICE_BASE64_64_AUTH`, not your account password | +| Worker repeatedly logs `tick failed` | Web logs, readiness, and the shared tick secret | +| Upload returns 413 | Proxy and application upload limits | +| Password-reset mail does not arrive | Saved mail configuration and its test button | +| Scheduled work is delayed | Worker status and **Admin → System** task results | + +See [Operations troubleshooting](../../guides/operations/operating.md#troubleshooting) +for application failures. ## Next -The board is up, and your part may be done: the three guides at the top of -this table need a browser and nothing else, so they can go straight to the -people doing those jobs. - -| You want to | Read | -|---|---| -| Hand it to whoever runs the community | [The organiser's guide](../../guides/community/organiser-guide.md) | -| Hand the queue to the moderators | [The moderator's guide](../../guides/community/moderation-guide.md) | -| Take memberships online | [The memberships guide](../../guides/community/membership-guide.md) | -| Browse and install a plugin or theme somebody else built | [The marketplace](../../customization/marketplace.md) | -| Run the server day to day | [Operations](../../guides/operations/operating.md) | -| Take it from one version to the next | [Upgrading a board](../../guides/operations/upgrading.md) | -| Deploy it without a panel | [Deploying by hand](./docker-compose.md) | -| Deploy it without a server | [Running on Vercel](./vercel.md) | -| Change how it looks | [The theme API](../../customization/themes.md) | -| Add behaviour | [The plugin API](../../customization/plugins.md) | -| Move a MyBB or phpBB forum here | [Migrating from MyBB or phpBB](../../guides/migrating.md) | -| Work on Meith itself | [Development](../../contributing/development.md) | +- [Set up your community](../first-steps.md). +- [Server operations](../../guides/operations/operating.md). +- [Upgrade Meith](../../guides/operations/upgrading.md). diff --git a/docs/getting-started/deployment/docker-compose.md b/docs/getting-started/deployment/docker-compose.md index 2369df91c..18cfdaadf 100644 --- a/docs/getting-started/deployment/docker-compose.md +++ b/docs/getting-started/deployment/docker-compose.md @@ -1,28 +1,7 @@ -# Deploying by hand - -**No panel.** The [Quickstart](./coolify.md) deploys a board with -[Coolify](https://coolify.io) — a guided panel that issues the -certificate, generates the secrets, and redeploys with one button — and -is the route most boards should take: same four containers, same -environment contract either way. This page deploys the identical -board — the workspace `npx create-meith` or -[the template](https://github.com/meith-dev/template) writes, the same -shape [the marketplace](../../customization/marketplace.md) installs -into — with Docker Compose alone. Take it if: - -- **you already run a proxy** (nginx, Traefik, Caddy) and would rather add - one vhost than a second thing that wants ports 80 and 443; -- **you want no extra moving parts** — Coolify is a daemon, a database and - a proxy of its own, a fair price for what it does and not free; -- **the machine is too small for it** — Coolify wants ~2 GB to itself; -- **you are deploying into something else** — an existing Swarm, a Nomad - job, a CI pipeline that already builds images. - -What you give up: Coolify's certificate, its generated secrets, its -redeploy button, and its own scheduled off-host backup. All four become -yours, and the first is the one people underestimate — the board still -takes its own backups either way, see [What you are taking -on](#what-you-are-taking-on). +# Deploy with Docker Compose + +Use this route to deploy a board with Docker Compose and a reverse proxy you +manage. For deployment through a browser panel, use [Coolify](./coolify.md). ## What you need @@ -74,44 +53,13 @@ curl -fsSL https://www.meith.dev/create-board.sh | bash -s -- my-board cd my-board ``` -Pick `my-board`'s replacement now — the name of the directory this writes -and, if you push it anywhere, of the repository on GitHub. It is not the -board's display name (the installer asks for that later, in [step -6](#6-install-it)), so it does not have to be pretty, only lower-case -with no spaces. `npx create-meith my-board` does the identical thing if -you already have Node.js and would rather use it. - -Either command writes a small workspace — `package.json`, -`meith.config.ts`, `board.plugins.json` — that depends on the published -`@meith/web` and `@meith/cli` packages instead of containing a copy of -this repository, which is what turns "installing a plugin" from a fork of -this project into `npm install` and a line in a config file. See -[Consuming the board from a workspace](../../contributing/development.md#consuming-the-board-from-a-workspace) -for the mechanism (`forum-web`/`meith` — the bins the compose file below -actually runs) and [the plugin API](../../customization/plugins.md) for -installing one once the board exists. - -The workspace carries a deploy kit with **three** routes onto a server, -not just this one — [Quickstart § Create your -board](./coolify.md#2-create-your-board) is where the other two are -written up in full: - -| File(s) | Route | -|---|---| -| `Dockerfile`, `docker-compose.yaml` | Coolify, building the image itself — the Quickstart's default | -| `Dockerfile.prebuilt`, `docker-compose.prebuilt.yaml`, `.github/workflows/build.yml` | Coolify, pulling an image GitHub Actions built — the Quickstart's advanced path | -| `docker-compose.byhand.yaml` | This page — no panel, a `.env` you write | - -A board takes one route at a time. **Delete `docker-compose.yaml` now**, -at least: it is the name Docker Compose guesses when nothing tells it -otherwise, and it has no fallback of its own for any secret — it expects -Coolify to have generated one. Leaving it in place is a loaded footgun for -step 4; deleting it removes the trap rather than asking you to remember it -is there. `Dockerfile`, `Dockerfile.prebuilt`, `docker-compose.prebuilt.yaml` -and `.github/workflows/build.yml` are harmless left in place — nothing -auto-discovers any of them the way Compose does its default filename — so -leave those for [Building somewhere else](#building-somewhere-else) -below, or delete them too if you already know you will not need them. +Use your own directory name in place of `my-board`. If Node.js 22 or newer +is installed, `npx create-meith my-board` creates the same board workspace. + +This route uses `Dockerfile` and `docker-compose.byhand.yaml`. The default +`docker-compose.yaml` is for Coolify; select the by-hand file with +`COMPOSE_FILE` in the next step so plain `docker compose` commands use the +right configuration. ## 3. Write the environment @@ -198,7 +146,7 @@ The first build takes five to ten minutes. Services come up in order: | `postgres` | The database. A named volume, so recreating the container keeps the data. | | `migrate` | Applies the schema and **exits 0**. `web` and `worker` wait for it, so the code never runs against a schema behind it. | | `web` | Next.js, on `127.0.0.1:3000`. | -| `worker` | Calls `/api/system/tick` over HTTP once a minute. `@meith/worker` is not published, so a board outside the meith monorepo drives the tick this way rather than running the compiled process — the same shape [Quickstart § 3](./coolify.md#3-set-your-domain-and-deploy) uses under Coolify. It never touches the database itself; only the request it makes does. | +| `worker` | Calls `/api/system/tick` over HTTP once a minute. `@meith/worker` is not published, so a board outside the meith monorepo drives the tick this way rather than running the compiled process — the same shape [Coolify § 3](./coolify.md#3-set-your-domain-and-deploy) uses under Coolify. It never touches the database itself; only the request it makes does. | Check all four: @@ -306,6 +254,9 @@ whose scripts the browser refuses. Open `https://board.example/install` — your domain, over the proxy you just set up, not `127.0.0.1:3000`. +Unlock the installer with the `AUTH_SECRET` stored in your `.env`. The +browser unlock lasts 30 minutes and is also required for restore. + The form is three numbered sections: **Your board** (its name), **Your account** (username, e-mail, password), and **Sending mail** (optional here, painful later). The board's address is not asked for — `APP_URL` @@ -320,15 +271,14 @@ lists them under the box. Fill in mail here too. It is a list of providers rather than a page of server details — pick the one you have and the host, port and TLS mode -come with it — and a **test message goes to your address before the first -migration**, installing nothing if it fails. +come with it — and a **test message goes to your address before any installation data is written**, installing nothing if it fails. Everything else about the installer — the preflight report, the five steps, the sealing that cannot be undone — is the same on both routes and written once: -- **[Quickstart § Run the installer](./coolify.md#4-run-the-installer)** -- **[Quickstart § Mail](./coolify.md#5-mail)** — the answer sheet for +- **[Coolify § Run the installer](./coolify.md#4-run-the-installer)** +- **[Coolify § Mail](./coolify.md#5-mail)** — the answer sheet for the provider list. `/admin/settings?group=mail` changes it afterwards with no redeploy; the `MAIL_*` variables in the `.env` beside this stack override both. @@ -404,7 +354,7 @@ image: ghcr.io//my-board:latest once the board is settled and you want upgrades happening only when you choose — a floating tag turns the next incidental `docker compose pull` into an unplanned upgrade, the same reasoning -[Quickstart § Set your domain and deploy](./coolify.md#3-set-your-domain-and-deploy) +[Coolify § Set your domain and deploy](./coolify.md#3-set-your-domain-and-deploy) walks through for the equivalent Coolify setting. ## When it goes wrong @@ -428,47 +378,15 @@ covers the failures that are about the board rather than the deployment. ## What you are taking on -Worth being plain about, because this is the route with no panel behind -it: - -- **Backups are yours.** Nobody else is taking one. The board schedules - its own — **Admin → Settings → Backups** — bundling the database *and* - the uploads into the `backups` volume and, once you name a bucket, off - the server; the bucket is still yours to rent. See - [Backups](../../guides/operations/backups.md), and the - [disaster-recovery runbook](../../guides/operations/disaster-recovery.md) for the day they - are all you have. -- **Certificates are yours.** Caddy makes it a solved problem, but it is - a problem you now own. -- **Security updates are yours.** `unattended-upgrades` for the host; a - `create-meith` update and a rebuild for the board. -- **Uptime is yours.** `restart: unless-stopped` covers a crash and a - reboot; it does not cover a disk filling up. The compose file caps what - each container may log, so a crash-loop cannot fill the disk by itself — - but the database and the uploads still grow, and watching the disk is - still yours. - -In exchange: no platform limits, no per-seat pricing, no vendor reading -your members' posts, and a board you can move to another machine with a -`pg_dump` and a `tar`. +Maintain the host, proxy certificates, board version, disk capacity, and +backups. Configure [Backups](../../guides/operations/backups.md) to include +both the database and uploads, copy bundles off-site, and test a restore. +Container restart policies do not replace monitoring or backups. ## Why a server, and not functions -You can run this board on functions, and [Running on -Vercel](./vercel.md) is that route written out: the driver set, the build -command that carries the migration, the cron job that stands in for the -worker, and how to leave again. - -A server is still the recommended default, and the reason is that -everything this page gives you for free becomes configuration and a -second bill there. A process that outlives a request is what a worker -*is*; without one, the tick is an HTTP endpoint somebody else's scheduler -has to call, at a cadence their plan decides. A disk that survives a -restart becomes an object store. A shared cache becomes a managed Redis. -The migration stops being a one-shot job beside the board and becomes -part of the build, which trades the deploy window for a different one -rather than closing it. None of that is unworkable — it is documented -because it works — but it is four vendors and a longer list of things to -get right, in exchange for not owning a machine. One machine, one -database, one `pg_dump` that is the whole board is the simpler answer, -and it is the one most boards should take. +A server can run scheduled work continuously and keep uploads on persistent +disk. [Vercel](./vercel.md) uses a scheduler and managed storage instead. +Choose the route whose services you can maintain and verify. + +Continue with [Set up your community](../first-steps.md). diff --git a/docs/getting-started/deployment/index.md b/docs/getting-started/deployment/index.md index 6e5e5ee5c..bde5f706a 100644 --- a/docs/getting-started/deployment/index.md +++ b/docs/getting-started/deployment/index.md @@ -1,43 +1,39 @@ -# Deployment - -A production Meith board is four services, whichever route puts them -there: - -- **PostgreSQL** stores everything the community makes. -- **A one-shot migration service** updates the schema, and finishes - before anything else starts. -- **The web service** answers browsers and the API. -- **The worker** runs scheduled and queued work once a minute — or, for a - board scaffolded outside this repository, a small loop that drives the - same work over HTTP instead, since the compiled worker process is not - something such a board depends on today. - -Every route below deploys that same shape, starting from the same -scaffold — `npx create-meith` or [the -template](https://github.com/meith-dev/template) — and ends the same way: -open `/install` on the new board to name it and create its first -administrator. Building the image on the server is the default and the -fastest way to a first deploy; a low-spec machine can instead pull one -built elsewhere, and each route below says how. +# Choose a deployment + +Choose where the board will run. For a local preview that needs no database, +use [Try Meith locally](../quickstart.md). ## Pick your route -| Route | For | You need | +| Route | Use it when | You manage | |---|---|---| -| [Coolify](./coolify.md) | Most boards — the guided route | A rented server and a domain; no terminal required after setup | -| [Docker Compose by hand](./docker-compose.md) | Operators who already run a proxy | Docker Compose, a `.env` you write, a reverse proxy you operate | -| [Vercel](./vercel.md) | Boards that would rather not have a server | A Vercel account and a hosted Postgres; the worker becomes a cron tick | +| [Coolify](./coolify.md) | You want a panel to deploy on your own server | A server, domain, board repository, and backups | +| [Docker Compose](./docker-compose.md) | You already manage Docker and a reverse proxy | Containers, secrets, HTTPS, and backups | +| [Vercel](./vercel.md) | You want the web app on managed functions | Hosted PostgreSQL, shared cache, object storage, mail, and a scheduler | + +Coolify and Docker Compose run four services: PostgreSQL, a one-shot +migration service, the web app, and a worker. The scaffold's worker calls +`/api/system/tick` over HTTP; the repository's image runs the worker process. + +Vercel uses a different deployment: migrations run before the build, the web +app runs in functions, and a scheduled HTTP call replaces the worker. +Read its [limits](./vercel.md#the-limits-worth-knowing-first) before choosing it. -If you only want something the public can poke, -[demo mode](../../guides/operations/demo-mode.md) runs a board that -resets itself on a schedule. +All routes use a board repository with pinned Meith packages. The installer +at `/install` creates the first administrator and forum after migrations +have completed. + +## Moving an existing forum + +Follow [Migrate from MyBB or phpBB](../../guides/migrating.md) after installing +the destination board. Rehearse against copies of the source +database and uploads, and review the feature differences linked from that guide. ## After it is up -- [Operations](../../guides/operations/operating.md) — health checks, - configuration, mail, backups, and the operator CLI. -- [Upgrading](../../guides/operations/upgrading.md) — moving between - released versions safely; the board, its plugins and the compose file - carry one version number and move together. -- [Monitoring](../../guides/operations/monitoring.md) — what to alert on - once people rely on it. +1. [Set up your community](../first-steps.md). +2. [Configure and test backups](../../guides/operations/backups.md). +3. [Check services and scheduled work](../../guides/operations/monitoring.md). + +For an isolated board that visitors can experiment with and that resets its +data, see [Demo mode](../../guides/operations/demo-mode.md). diff --git a/docs/getting-started/deployment/vercel.md b/docs/getting-started/deployment/vercel.md index 4c804ad22..556ca3375 100644 --- a/docs/getting-started/deployment/vercel.md +++ b/docs/getting-started/deployment/vercel.md @@ -1,21 +1,10 @@ -# Running on Vercel +# Deploy on Vercel -A Meith board runs on Vercel from a Deploy Button: one click provisions the -database, cache, object store and mail account, you generate two secrets, -and the board is live. [Deploy it](#deploy-it) is that route, and it is -four steps long. - -The rest of this page is the part worth reading before you commit — what -this route cannot do, what it costs, and the handful of places where a -platform of functions behaves differently from a server you own. It ends -with [how to leave](#leaving-vercel), because that is what decides whether -this is a home or a trap. - -This is a narrower route than the [Quickstart](./coolify.md), not a -better one. There is no server to SSH into, no worker process, no disk, and -no `docker compose run` to reach for when something needs a command run -against the board. What you get in exchange is that none of those are yours -to keep alive. +Deploy the web app on Vercel with managed PostgreSQL, Redis, object storage, +and mail. Read the [limits](#the-limits-worth-knowing-first) and +[scheduler requirements](#the-tick-replaces-the-worker) first. The template's +daily cron is suitable for trying the deployment; active boards usually need +a more frequent tick. ## Deploy it @@ -48,16 +37,14 @@ suggestion. Vercel's own cron documentation suggests a 16-character `CRON_SECRET`; that value is refused here. Generate each separately. **4. Deploy, then open `https://your-deployment/install`.** The build -applies the schema and builds the board; the installer asks for the board's -name, your account, and nothing else. When it finishes it seals itself and +applies the schema and builds the board. Unlock the installer using the +`AUTH_SECRET` you saved in step 3, then enter the board name and your account +details. Review the supplied URL and mail settings. When it finishes it seals itself and `/install` answers 404 from then on. -That is the whole route. Everything the board needs beyond those two -secrets is derived from what the four products publish — the drivers, both -database strings, the Redis URL, and the mail sender — which is why the -form asks for two fields rather than sixteen. [What the board looks -for](#what-the-board-looks-for-and-in-what-order) is the full order of -resolution, and matters only when something does not resolve. +The board derives driver configuration from the connected services. See +[environment resolution](#what-the-board-looks-for-and-in-what-order) if a +value is missing. > [!NOTE] > **`APP_URL` is the one value to check afterwards.** Every link in every @@ -67,64 +54,26 @@ resolution, and matters only when something does not resolve. ## Before you commit to this route -Whether this route suits the board you have in mind is decided by what it -is for, what it costs, and what it cannot do. The third is the one to read -twice: those limits are properties of running on functions, and no amount -of configuration moves them. - ### Who this route is for -Take it if: - -- **you have no server and no wish to acquire one.** Nobody on the - committee has to learn `ufw`, renew a certificate, or notice that a disk - filled up. -- **traffic is bursty.** A board that is quiet for six days and busy on - club night pays for the busy part rather than for a machine sized to it. -- **you are already on Vercel** and adding one more project is less work - than adding the first server. - -Do not take it if: - -- **where the data lives is the point.** This route spreads a board across - a platform, a managed database, a managed cache and an object store — - four companies holding your members' posts, none of them you. If that - sentence is the reason your community is leaving whatever it is leaving, - stop here and read [Deploying by hand](./docker-compose.md) instead. One - machine you rent, one database on it, one `pg_dump` that is the whole - board. That is the honest answer to data sovereignty, and this page is - not it. -- **you want the documented default.** A server is still what most boards - should run, and what most of this documentation assumes. -- **you are importing a large MyBB or phpBB board.** The importer is a - long-running command against two databases; see - [the limits](#the-limits-worth-knowing-first). +Use this route when you want managed hosting or already operate on Vercel. +There is no persistent server or local upload disk. Operator commands run +from a board checkout with access to the hosted services. +For a server you manage, use [Coolify](./coolify.md) or +[Docker Compose](./docker-compose.md). -> [!NOTE] -> **This route is not covered by an automated deployment test.** The -> drivers underneath it are — the cache contract suite runs against a real -> Redis-compatible server on every CI run, and the environment rules below -> are unit-tested — but nothing in CI deploys a board to Vercel and checks -> that it came up. Treat a Vercel deploy as something you verify yourself, -> the same way you would verify any deployment nobody has rehearsed for -> you. +CI tests the drivers and environment rules, but does not deploy to a live +Vercel account. Verify the completed deployment before inviting members. ### What it costs -Usage-based, and spread across four bills rather than one: +Budget for web functions, PostgreSQL, Redis, object storage, mail, and the +scheduler. These may be separate services or bundled by a provider. Check +the providers' current plans against your traffic and storage requirements. -| Service | What it is | Notes | -|---|---|---| -| Vercel | Serving the board, and the cron scheduler | A tick faster than daily needs a paid plan — see [the tick](#the-tick-replaces-the-worker) | -| Managed PostgreSQL | Everything durable: posts, members, sessions, the queue | Needs both a pooled and a direct connection string | -| Managed Redis | The shared cache, and nothing else | Losing it costs a warm cache, not data | -| Object storage | Avatars and attachments | A Vercel Blob store, which is on the Vercel bill and provisions itself, or any S3-compatible bucket: R2, S3, Spaces, MinIO | - -Some providers bundle two of these, which makes it three bills rather than -four. None of them bundle all of it. A single rented server running the -[Quickstart](./coolify.md) is one bill, a fixed one, and usually a -smaller one — the case for this route is the operational work it removes, -not the money. +Vercel Hobby cron runs at most daily. More frequent Vercel cron schedules +require a suitable paid plan, or you can use an external scheduler. +See [Vercel's cron limits](https://vercel.com/docs/cron-jobs/usage-and-pricing). ### The limits worth knowing first @@ -137,15 +86,12 @@ does. Run it from a checkout of your board repository, pointed at the same `DATABASE_URL`, and follow [Migrating from MyBB or phpBB](../../guides/migrating.md). -**Uploads and downloads both buffer wholly in function memory.** The board -uploads each object in a single request rather than a multipart one, -holding the whole file in memory while it is processed and sent; reads have -the same ceiling, because the download route buffers the whole object -before it answers. So **the function's memory limit — not the bucket — -caps attachment size, in both directions**. An attachment uploaded on a -larger function will exhaust a smaller one on the way back down. Set the -board's own attachment limit below what the function can hold, and remember -it applies to serving as well as receiving. +**Request and response size limits apply before memory limits.** Vercel +Functions limit request and response payloads to 4.5 MB. Allow for the complete +form payload, including multiple attachments and multipart overhead. Meith +also buffers uploaded and downloaded files in function memory. Raising the +board's attachment limit or the function's memory does not bypass the +platform payload limit. See [Vercel Function limits](https://vercel.com/docs/functions/limitations). **Redis connections scale with concurrent instances**, and the platform decides how many of those exist. A traffic spike that creates two hundred @@ -164,10 +110,6 @@ rather than inside a container on a server. ## Things to know -Each of these is a property of running on functions rather than a bug, and -each one has surprised someone. Nothing here is needed to deploy — come -back to it when something behaves unlike the server you expected. - ### The tick replaces the worker A Compose deployment has something ticking every 60 seconds without being @@ -268,124 +210,47 @@ means to a scheduler. ### What build-time migration means -Welding the migration to the build buys the `&&` guarantee, and it costs -three things. All three are properties of the arrangement rather than bugs, -and [Upgrading § When the build runs the migration](../../guides/operations/upgrading.md#when-the-build-runs-the-migration) -is the full treatment. +The template runs `meith migrate && forum-web build --at-root`. If migration +fails, the build stops. If migration succeeds but the build fails, the old +code continues serving against the migrated schema. #### The deploy window is inverted, not closed -When the deploy and the migration are separate events, new code serves -against an old schema until somebody runs the command. Build-time migration -does not remove that window — it turns it around. The migration runs during -the build, while the **previous** deployment is still serving, so between -the migration and the cutover it is **old code against a new schema**. - -For a release that only adds things, that is safe. For one that removes or -renames, the two-step rule still holds but you no longer get to order its -steps, which leaves a single invariant: - -> A release's migration must be tolerated by the release *before* it, -> because that is the code serving while this release's build migrates. - -So a destructive migration cannot travel in the same release as the code -that tolerates it. Those have to be two deploys. +Keep migrations compatible with the currently serving release: it remains +live during the build. Split destructive changes across compatible releases. #### Every build migrates, previews included -The build command is the build command. It runs for every deployment the -platform builds: the pull-request preview, the branch deployment, the -redeploy of an old commit. Each one runs `meith migrate` against -whatever database that deployment's own environment variables name. - -This is where the pattern cuts, and Vercel's default is on the wrong side -of it. **Vercel documents that a new environment variable applies to all -environments unless you narrow it**, which points preview and branch builds -at the production database — and then the first preview build of an -unmerged branch migrates production, from a schema nobody has reviewed, -with no deploy of that branch ever having happened. Nothing in the build -command can detect this: from the migration's point of view it is an -ordinary run against an ordinary `DATABASE_URL`. - -> [!CAUTION] -> **Scope `DATABASE_URL` and `DIRECT_DATABASE_URL` to Production only**, -> and give preview and branch environments a database of their own — a -> separate instance, or a branch of the managed one where the provider -> offers that. Check the scoping before the first preview build rather than -> after. By the time it is visible the migration has applied, and a -> migration does not come back off. - -Overlapping deploys themselves are safe. Two builds triggered close -together queue on the advisory lock, and the second finds the schema -current and applies nothing. +Give previews and branch deployments separate databases. Check both +`DATABASE_URL` and `DIRECT_DATABASE_URL`; every build runs migrations against +the database those variables name. Core migrations serialize through a +session-level advisory lock; apply plugin upgrades one at a time. #### Rollback does not un-migrate -Vercel documents its instant rollback as promoting a previous deployment -by re-pointing an alias at an artefact that was built already. That -**runs no build**, so it never calls `meith migrate`. There is nothing to undo -the schema with. Rolling back the other way, by redeploying an older -commit, does build and does run `meith migrate`, which then applies -nothing, because migrations are forward-only. - -Either route puts the old code back and leaves the schema where it is. A -rollback is therefore only safe while the older code tolerates the newer -schema. - -There is one more shape to know: **a successful migrate followed by a -failed build**. The `&&` guards one direction only. It stops new code -reaching an old schema and does nothing about the reverse, so the -deployment aborts with the migration already applied and the previous -release still serving — and it stays that way until some later build -succeeds. The instinct is to roll back, and rolling back does nothing: the -old code is already what is serving. Fix the build and deploy forward. +An instant rollback changes the deployed code, not the schema. Redeploying +an old commit does not reverse migrations either. Use a backup to recover +from an incompatible schema change. + +See [Upgrading — build-time migrations](../../guides/operations/upgrading.md#when-the-build-runs-the-migration) +for the upgrade procedure and rollback limits. ### The installer, and the four things specific to here -Everything about the installer is the same here as on every other route and -is written once, in -[Quickstart § Run the installer](./coolify.md#4-run-the-installer): the -preflight report that separates blockers from warnings, the three form -sections, the five steps, and the sealing that cannot be undone. Read that, -then come back for the four things specific to this route: - -- **The board's address is not asked for.** `APP_URL` supplies it, and the - preflight names the value it is using. Check that line — a preview URL - left in `APP_URL` is a board whose password-reset links point at a - deployment that will not exist next week. -- **The installer checks the schema rather than applying it.** Its first - step confirms every table the board needs is there and stops with the - names of any that are not; it never migrates. The build command already - did that — `meith migrate && forum-web build --at-root` — and a - serverless function is the wrong place to try: several cold starts would - contend for the same migration lock, and the function timeout bounds how - long a migration is allowed to take. The step reads the table names out - of the schema definitions, which are ordinary imported code, because the - migration `.sql` files are not in the function: nothing imports them, so - nothing traces them in, and nothing lists them in - `outputFileTracingIncludes` either. Listing them there would not have been - inert, for the record — Turbopack emits its trace files and applies those - globs whether or not `output` is `'standalone'`, which is the configuration - Vercel builds ([Building where Vercel - looks](../../contributing/development.md#building-where-vercel-looks)) — so - this is a design choice rather than the only option: a schema check needs no - files and cannot contend for the migration lock. If that step does report - missing tables, run `meith migrate` against the same database and - reload — `MIGRATIONS_DIR` cannot help when the files are absent. -- **The installer takes the same session-level advisory lock migrations - do**, so it needs `DIRECT_DATABASE_URL` for the same reason. Run against - a pooler, it can report itself permanently in flight. -- **A warning about `TICK_SECRET` means what it says, not that the tick is - unprotected.** The preflight checks that one variable by name, so a board - configured the way Vercel Cron needs — `CRON_SECRET` and nothing else — - is warned that the tick has no secret while the tick is in fact guarded. - It is a warning rather than a blocker, so you can install straight past - it. Setting `TICK_SECRET` as well, as [the environment](#the-environment-variable-by-variable) - recommends, is the tidier answer and clears the check. - -Sealing is recorded in the database rather than in the deployment, so it -survives every redeploy: `/install` answers 404 from then on, however many -times the project builds afterwards. +Use the [installer procedure](./coolify.md#4-run-the-installer), with these +Vercel-specific checks: + +- Unlock `/install` with the deployment's `AUTH_SECRET` before installing or + restoring. The browser unlock expires after 30 minutes. +- Check `APP_URL`; email links must point at the public board, not a preview. +- Core migrations run during the build. The installer checks the resulting + schema and does not apply migrations itself. +- Set `DIRECT_DATABASE_URL` for the installer's session-level lock. The + preflight checks `TICK_SECRET` by name and may warn if you set only + `CRON_SECRET`, even though the endpoint accepts either secret. Setting + both clears that warning. + +After installation, the seal is stored in PostgreSQL and survives redeploys. ### Mail @@ -585,7 +450,7 @@ nothing. The lists live in `packages/core/src/env.ts` as `VERCEL_REDIS_URL_SOURCES`, `VERCEL_DIRECT_DATABASE_URL_SOURCES`, -`VERCEL_BLOB_TOKEN_SOURCES` and `VERCEL_BLOB_STORE_MARKERS`. If your +and `VERCEL_BLOB_CREDENTIAL_SOURCES`. If your provider publishes a name that is not on one of them, the shortest fix is to set `REDIS_URL` or `DIRECT_DATABASE_URL` yourself — the derivation stands aside for anything already set — and the durable one is to add the @@ -657,7 +522,7 @@ What you have to bring, which the Deploy Button would otherwise provision: | | | |---|---| -| **A board repository** | A scaffolded board of your own, not a clone of the Meith repository — the same workspace [Quickstart § 2](./coolify.md#2-create-your-board) creates. It depends on the published `@meith/web` and `@meith/cli` packages, which is what puts the `forum-web` and `meith` commands in the build. | +| **A board repository** | A scaffolded board of your own, not a clone of the Meith repository — the same workspace [Coolify § 2](./coolify.md#2-create-your-board) creates. It depends on the published `@meith/web` and `@meith/cli` packages, which is what puts the `forum-web` and `meith` commands in the build. | | **A managed PostgreSQL** | With both connection strings: the transaction-mode pooler and the direct one. Both are needed, for the reason under [why both database strings](#why-both-database-strings). | | **A managed Redis** | Reachable over TLS (`rediss://`). | | **Somewhere to put uploads** | Either a Vercel Blob store, which costs nothing to set up, or an S3-compatible bucket and a key pair for it. The choice has consequences for [leaving](#leaving-vercel); read that first. | @@ -833,7 +698,7 @@ above; [Backups](../../guides/operations/backups.md) is the reference. ### 2. Stand up the destination Follow [Deploying by hand](./docker-compose.md) — a server, the compose file, -a `.env` and a proxy — or the [Quickstart](./coolify.md) if you would +a `.env` and a proxy — or [Coolify](./coolify.md) if you would rather have the panel. Write the `.env`, and then **bring up Postgres alone**: @@ -854,7 +719,7 @@ stack. You are restoring a board, not installing one — though the installer can do it for you: bring the whole stack up instead, set the `BACKUP_S3_*` values so the fresh board can see the bucket the bundle shipped to, open -`/install`, and pick the bundle under **Or restore a backup**. That route +`/install`, unlock it with `AUTH_SECRET`, and pick the bundle under **Or restore a backup**. That route is [Restoring from the installer](../../guides/operations/backups.md#from-the-installer); the one below is the same restore from a shell. @@ -918,7 +783,7 @@ restore run against a destination that keeps the objects. | The build fails naming `BLOB_STORE_ID` and `BLOB_READ_WRITE_TOKEN` | `FILESTORE_DRIVER=blob` is set but no Blob store is attached to the project, or it was attached after this build's environment was read. Attach one under **Storage**, then redeploy. | | The board boots, then an upload fails saying the store was reached with no usable credential | The board is on the OIDC path and the platform supplied no identity token — OIDC is off for the project, or this is running off the platform, as a local `meith backup` is. Create a read-write token on the store and set `BLOB_READ_WRITE_TOKEN`. | | A refusal names a variable your store does publish, under another name | The candidate list does not have that name. Set `REDIS_URL` or `DIRECT_DATABASE_URL` directly — an explicit value stands the derivation down — and [add the name to the list](#what-the-board-looks-for-and-in-what-order). | -| The board boots but sends no mail | No mail token is set, so `MAIL_DRIVER` fell back to `log` and every message goes to the build log. Add Resend to the project, or set `MAIL_HTTP_ENDPOINT` and `MAIL_HTTP_TOKEN`. A sender is needed too: `RESEND_EMAIL_DOMAIN` derives one, or set `MAIL_FROM` by hand. | +| The board boots but sends no mail | No mail token is set, so `MAIL_DRIVER` fell back to `log` and every message goes to the runtime log. Add Resend to the project, or set `MAIL_HTTP_ENDPOINT` and `MAIL_HTTP_TOKEN`. A sender is needed too: `RESEND_EMAIL_DOMAIN` derives one, or set `MAIL_FROM` by hand. | | Mail is rejected with a sender error | The sender is at a domain the provider has not verified — whether it came from `MAIL_FROM` or was derived from `RESEND_EMAIL_DOMAIN`. Verify it in the provider's dashboard; nothing on this side can work around it. | | Production migrated and nobody deployed anything | A preview or branch build did it, because the database variables reach every environment — [scope them to Production](#every-build-migrates-previews-included). The migration has applied and does not come back off. | | A rollback did not fix the schema | It never could. Rollback runs no build and so runs no migration — [above](#rollback-does-not-un-migrate). Deploy forward. | diff --git a/docs/getting-started/first-steps.md b/docs/getting-started/first-steps.md new file mode 100644 index 000000000..2b9c5b36e --- /dev/null +++ b/docs/getting-started/first-steps.md @@ -0,0 +1,76 @@ +# Set up your community + +Use this checklist after completing `/install`. You need the administrator +account created by the installer. If the board is not installed yet, +[choose a deployment](./deployment/index.md) first. + +## 1. Open the admin panel + +Sign in to the board, then open `/admin`. Enter your password again to open +the admin panel's separate session. On a small screen, use the panel menu +to reach its sections. + +## 2. Check the name, appearance, and rules + +- **Admin → Board settings → Board**: set the board name and description. +- **Admin → Themes**: upload a logo and choose the default theme. You can + adjust its colours and fonts without deploying new code. +- **Admin → Board settings → Legal**: publish your community's Rules & FAQ, + terms of service, and privacy policy. Non-empty pages appear in the footer. + +[Community administration](../guides/community/organiser-guide.md#making-the-board-look-like-yours) +explains the appearance controls. + +## 3. Test mail before opening registration + +Open `/admin/settings?group=mail`, save the sending settings, and select +**Send a test message to me**. Check that it arrives. + +Under `/admin/settings?group=registration`, choose whether registrations are +open and whether accounts need email confirmation, administrator approval, +both, or neither. Email confirmation and password reset need working mail. +If delivery fails, follow [Mail](../guides/operations/operating.md#mail). + +## 4. Create the forums + +The installer creates a first forum. At **Admin → Forums**, rename it or +add categories and forums for your community. Keep the initial list small +enough that new members can find where to post. + +For a private staff forum, create the staff group, assign its members, then +set the forum's permissions. Follow the +[private forum procedure](../guides/community/organiser-guide.md#a-private-forum-for-the-organisers): +a deny applies to one group's contribution, and another group can still grant access. + +## 5. Assign moderators + +Open each forum in **Admin → Forums** and appoint its moderators. Choose +the actions they need; deleting and restoring posts are separate permissions. +Give moderators the [Moderation guide](../guides/community/moderation-guide.md). + +## 6. Check the member experience + +Create an ordinary member account and check the board with it, then sign out +and check as a guest. An administrator can bypass forum restrictions, so an +administrator account alone cannot prove a private forum is private. + +Check that: + +- Guests and ordinary members see only the intended forums. +- A member can create a thread and reply where permitted. +- Registration and password reset messages arrive. +- The rules and navigation links lead to the right pages. + +Post a welcome thread with the community's purpose and a link to its rules. +The [Member guide](../guides/community/member-guide.md) covers the common tasks. + +## 7. Confirm the handover + +Ask the operator to confirm that scheduled work is running, an off-site +backup has completed, and a restore has been tested. Use +[Monitoring](../guides/operations/monitoring.md), +[Backups](../guides/operations/backups.md), and +[Disaster recovery](../guides/operations/disaster-recovery.md) for those checks. + +For ongoing administration, continue with +[Community administration](../guides/community/organiser-guide.md). diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md index 299459f76..7cb47b6c7 100644 --- a/docs/getting-started/introduction.md +++ b/docs/getting-started/introduction.md @@ -1,85 +1,46 @@ -# Introduction +# What is Meith? -Meith is open-source, self-hosted forum software for conversations worth -keeping — built for communities and owned by them. It is code-first, -built on modern TypeScript, and quick enough to run with JavaScript -disabled. It gives a community threaded discussions, search, permissions, -moderation, paid memberships, themes, plugins and an operator CLI — on a -server you control, with no per-member pricing, under the MIT licence. +Meith is open-source forum software. A community runs its own board, with +forums, discussions, member accounts, search, and moderation. It is written +in TypeScript and released under the MIT licence. -This page is the philosophy. If you would rather see it than read about -it, [the quickstart](./quickstart.md) has a board running on your machine -in about a minute, and [demo.meith.dev](https://demo.meith.dev) is a real -one you can open now. +## The basics -## Server-first +- A **board** is the community's website. +- A **category** groups related forums. +- A **forum** holds discussions about a subject. +- A **thread** is a discussion, beginning with a post and followed by replies. +- A **group** gives its members permissions, such as access to a private forum. -Every page a member reads is rendered on the server. The browser gets -HTML, not an application: threads, search, the composer and the admin -screens all work with JavaScript switched off, and script only layers -convenience — live previews, mention suggestions — on top of pages that -already work without it. That baseline is not a fallback mode; it is how -the board is built, and the e2e suite runs mostly with JavaScript off to -keep it true. +Most reading and posting works without JavaScript. Features such as live +previews and mention suggestions add convenience when JavaScript is available. -Being server-first is also why the board stays fast. There is no bundle -to hydrate before the first paint, and the performance budgets are -enforced the other way around: [the performance -reference](../reference/performance.md) is generated from measured runs -against a board with years of history in it, and a release that breaks a -budget is never published. +## Who does what -## Code-first +| Role | Work | Guide | +|---|---|---| +| Member | Read, post, follow discussions, and manage an account | [Member guide](../guides/community/member-guide.md) | +| Moderator | Approve content, handle reports, and manage discussions | [Moderation](../guides/community/moderation-guide.md) | +| Administrator or organiser | Manage members, forums, permissions, and board settings | [Community administration](../guides/community/organiser-guide.md) | +| Operator | Deploy, update, monitor, and back up the board | [Server operations](../guides/operations/operating.md) | +| Developer | Build extensions or change Meith itself | [First plugin](../customization/first-plugin.md) or [Development](../contributing/development.md) | -A board is a small repository, not a snowflake server. What the board -*is made of* is pinned in code and versioned: +One person can fill several roles. Access depends on the permissions granted +by the board, including appointments to moderate particular forums. -- `package.json` pins the engine — `@meith/web`, the CLI and the default - theme, at one exact version that moves only when you upgrade. -- `meith.config.ts` registers the themes the board ships and which - one is the default, statically, so the compiler checks the lot. -- `board.plugins.json` names the installed plugins, and a generated - registry makes them part of the build. +## What you install -Everything the community *does* — forums, permissions, groups, members, -settings, and every thread — lives in PostgreSQL and is run from the -browser by the people who run the community. The line between the two is -deliberate: a deploy can never delete a forum, and an organiser can never -break the build. [Configuration in code](../guides/configuration.md) -walks the whole boundary. +A board repository pins the Meith engine and registers its themes and plugins. +PostgreSQL stores accounts, posts, permissions, and board settings. Uploaded +files live in a local volume or an object store and must be backed up too. +See [Board configuration](../guides/configuration.md) for the files involved. -The same contracts are what you extend. Themes fill -[documented, versioned slots](../customization/themes.md) with typed view -models; plugins attach to [typed hooks](../customization/plugins.md) and -are isolated so one that crashes fails alone; and -[a REST API](../reference/api.md) covers anything an administrator can do -by hand. +Themes change the presentation. Plugins add features; paid memberships, for +example, use the **Dues** plugin and require payment setup. -## Self-hosted by default +## Start here -A production board is four containers — PostgreSQL, a one-shot migration -service, the web app and a worker — on a machine your community rents. -There is no company in the middle: the bill follows the server, never the -membership, and when the people running it change, the board is handed -over whole. [Deployment](./deployment/index.md) has three routes up, -from a guided panel to a compose file you operate yourself; there is also -[a serverless route on Vercel](./deployment/vercel.md) for boards that -would rather not have a server at all. - -Self-hosted does not mean self-reliant. Upgrades are versioned and -documented, backups are CLI commands, and -[the operations guides](../guides/operations/operating.md) assume the -person minding the machine has an evening a month for it, not a pager. - -## Where to go next - -- [Quickstart](./quickstart.md) — a board on your machine in about a - minute, no database required. -- [Deployment](./deployment/index.md) — the same board on your own - domain. -- [Configuration in code](../guides/configuration.md) — what the board - repository holds and why. -- [Themes](../customization/themes.md) and - [plugins](../customization/plugins.md) — the extension contracts. -- [Migrating](../guides/migrating.md) — moving a MyBB or phpBB board - across whole, working passwords included. +- To try Meith on your computer, use [Try Meith locally](./quickstart.md). +- To host a community, [choose a deployment](./deployment/index.md). +- If the board is already installed, [set up your community](./first-steps.md). +- If you are joining someone else's board, use the [Member guide](../guides/community/member-guide.md). diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 313a41152..0a9e22432 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -1,8 +1,7 @@ -# Quickstart +# Try Meith locally -A Meith board running on your machine, in about a minute, with nothing -installed but Node.js 22 or newer. No database, no Docker, no account -with anybody. +You need Node.js 22 or newer. These commands create a local preview with +sample data; no database is required. ## Scaffold a board @@ -13,13 +12,8 @@ npm install npm run dev ``` -Open . That is a board of your own: a workspace -that pins the engine at one exact version in `package.json`, with -[the config files](../guides/configuration.md) that decide its themes -and plugins, a Dockerfile, and a compose file for when it goes to a -server. The same scaffold comes from -`curl -fsSL https://www.meith.dev/create-board.sh | bash -s -- my-board` -if you would rather not go through npm. +Open . The generated directory contains your board's +[configuration files](../guides/configuration.md) and a deployment kit. With no `DATABASE_URL` set, the dev server runs in **fixture mode**: the board serves a realistic, deterministic dataset from memory — forums, @@ -28,28 +22,14 @@ page without PostgreSQL. Fixture mode deliberately does not fake writes; the moment you want to post rather than look, [point it at a database](../contributing/development.md#fixture-mode). -## Or run the engine itself - -Working on Meith rather than on a board? Clone the repository and the -same fixture-mode start applies to the whole workspace: - -```sh -git clone https://github.com/meith-dev/meith -cd meith -pnpm install -pnpm dev -``` - -[Development](../contributing/development.md) covers the Postgres-backed -setup, the tests, and what `pnpm verify` checks before a pull request. - ## Make it real When the board should be reachable by more people than you: -- [Deployment](./deployment/index.md) — the production shape and three - routes onto your own server, from a guided panel to a compose file. +- [Deployment](./deployment/index.md) — choose Coolify, Docker Compose, or Vercel. - [Configuration in code](../guides/configuration.md) — what to commit, what stays in the admin panel. - [Migrating](../guides/migrating.md) — if the community already lives on - a MyBB or phpBB board, bring it across whole instead of starting empty. + a MyBB or phpBB board, check import coverage and rehearse the move. + +To work on Meith itself, use [Development](../contributing/development.md). diff --git a/docs/guides/community/antispam.md b/docs/guides/community/antispam.md index 8524a7b7d..aceafd7f5 100644 --- a/docs/guides/community/antispam.md +++ b/docs/guides/community/antispam.md @@ -1,9 +1,7 @@ # Spam controls and filters -Everything a board has for keeping automated traffic out, bounding what -one visitor can do in an hour, rewriting words a reader should not meet, -and turning somebody away before an account exists. The rate limits and -challenges live in three places: +Use these controls to limit automated registration, posting, and other +requests. Configuration is split across: - **`/admin/antispam`** — the registration questions themselves. - **`/admin/settings?group=antispam`** — every threshold on this page. @@ -13,17 +11,13 @@ challenges live in three places: [The word filter](#the-word-filter) and [ban filters](#ban-filters), at the end of this page, have screens of their own under `/admin`. -Most of it ships switched off — a fresh board has no spam on it, and a -feature that arrives switched on introduces itself by breaking your -registration form. What ships on is what no human ever notices: the -hidden-field trap, a three-second minimum fill time, and the four -pre-authentication limits below. +The defaults enable the hidden-field trap, a three-second minimum fill time, +and pre-authentication limits. Other controls start disabled; the table +below lists their purpose and defaults. -> [!NOTE] -> **The counters live in the database**, so every instance of your board -> shares one allowance — and a board running without Postgres (fixture -> mode, `pnpm dev`, the demo) has no counters and applies none of the -> rate limits on this page. +Rate-limit counters are stored in PostgreSQL and shared across web instances. +Fixture mode has no database counters. [Demo mode](../operations/demo-mode.md) +uses PostgreSQL and has counters; it is separate from fixture mode. ## What each control is worth diff --git a/docs/guides/community/formatting.md b/docs/guides/community/formatting.md index e2b78cb8f..91e9b8fa9 100644 --- a/docs/guides/community/formatting.md +++ b/docs/guides/community/formatting.md @@ -1,15 +1,12 @@ -# Writing a post - -Posts, replies, private messages and signatures are all written in -Markdown. This page is the member-facing reference: what the composer's -toolbar buttons do, and the syntax behind each of them — including the -newer parts, which the toolbar's own **Formatting help** disclosure only -has room to name in passing. - -Everything on this page still works with JavaScript off. The toolbar, -the live preview, mention suggestions and the "Insert attachment" button -are enhancements layered on top of a composer that is a plain `