chapteros

Self-hostable member portal for networking organisations with directory, referrals, events and presentations

chapteros preview

Overview

Summary generated from the README

ChapterOS is a single-organisation member portal built on Payload 3 and Next.js 15 with Postgres and Supabase storage. It covers a member directory, referral lists, special requests, events with registration, slideshow presentations, and content such as blog, wiki and FAQ. It requires a Supabase project (or Postgres plus storage adapter) and an SMTP provider, and includes a setup wizard and Docker and Vercel deployment paths.

Features

  • Member directory with profiles, companies, photos and contact details
  • Top 40 / Top 20 referral lists with optional AI industry tagging
  • Special requests and events with registration and calendar invites
  • Slideshow presentation mode for member and power-group meetings
  • Blog, wiki, FAQ, success stories and policy templates with placeholders
  • Setup wizard, migrations, database lockdown script, and Docker and Vercel deployment

Good for

  • Running a member portal for a business networking chapter
  • Building a self-hosted membership site with directory, events and referral tracking
  • Forking a multi-feature Payload and Next.js app for a professional association

From the author

Self-hostable member portal for professional networking organisations — directory, referrals, one-to-ones, events and business tracking.

Get started

This repository is a GitHub template: create your own copy of it in one click.

Create a repository from this template

README

ChapterOS

A self-hostable member portal for professional networking organisations — the kind that meet regularly, do business with each other, and want their own site rather than a seat in someone else's platform.

Built on Payload CMS 3 and Next.js 15. One install serves one organisation: its members, its content, its branding, administered from the standard Payload admin panel.

Status: early. Extracted from a production system that has been running for several organisations, then generalised and reduced to a single-organisation install. The feature set is mature; the packaging for outside installs is new, and rough edges are likely.

Development is closed. The source is published under the AGPL and you are free to run, fork and modify it — but this repository does not take outside contributions. Issues are turned off and pull requests are not reviewed or merged. See CONTRIBUTING.md for what that means in practice, and SECURITY.md for the one channel that does stay open: private vulnerability reports.


What it does

Members Member directory with profiles, companies, photos and contact details. Profiles are member-editable; visibility is controlled under Settings.

Top 40 / Top 20 Each member maintains a list of companies and contacts they want to be introduced to. Other members search across all lists to find where they can help. Optional AI tagging classifies entries by industry so the search works on meaning rather than exact company names.

Special requests Short-lived "I'm looking for X" posts, visible to the membership.

Events Event pages with registration, guest submissions and calendar invites.

Presentations A slideshow mode that renders member and power-group slides for meetings — photos, business figures, what each member is looking for.

Content Blog, wiki, FAQ, success stories, static pages, and a policy-template system with placeholder substitution.


Requirements

  • Node.js 20.9+ (or 18.20.2+)
  • pnpm 9 or 10
  • A Supabase project — provides both Postgres and object storage
  • An SMTP provider — sends invitations and password resets

Any Postgres works if you prefer to run it yourself, but file uploads target Supabase Storage out of the box, and that is the combination the project guarantees. See What is supported before choosing otherwise.

Any provider speaking SMTP works. This project is developed against Mailjet, which has a free tier large enough for a chapter, and .env.example carries its settings as the worked example. The site starts and runs without one — it warns on every start and then cannot invite a member or reset a password, which is most of what a member portal does.

Not required: a Cloudflare account, or any other CDN. The rate limiter reads cf-connecting-ip if it happens to be behind Cloudflare, the same way it reads Vercel's and nginx's headers, and falls back to the socket address when there is nothing in front of the app at all.


Install

git clone https://github.com/igstudija/chapter-web.git
cd chapter-web
pnpm install
pnpm wizard

pnpm wizard asks for one setting at a time, checks each answer against the thing it describes before moving on, writes .env, and then runs the remaining steps in the order they have to happen in. A wrong password is one sentence at the prompt rather than a puzzle three commands later.

It still needs the accounts below — it can check a Supabase project and a storage bucket, not create them.

Run it yourself rather than having a coding agent run it. The answers are credentials, and an agent's transcript is the last place they belong. The wizard never prints a secret back: a value already in .env is offered as [kept], and what you type is not echoed.

It needs a terminal, and says so instead of hanging if it does not have one. For an unattended install — or to see exactly what it does — here is the same thing by hand.

The same install, step by step
cp .env.example .env

Fill in .env. The five that matter to start:

VariableWhere to get it
PAYLOAD_SECRETopenssl rand -base64 32
POSTGRESS_DATABASE_URLSupabase → Project Settings → Database → Connection string (URI)
SUPABASE_URLSupabase → Project Settings → API → Project URL
SUPABASE_SERVICE_ROLE_KEYSupabase → Project Settings → API → service_role
SUPABASE_STORAGE_BUCKETthe bucket you make below — media unless you rename it

In Supabase, create a public storage bucket named media. It must be public: files are served directly from Supabase, so a private bucket makes every image render broken rather than protected.

Then:

pnpm diagnose          # check those values before relying on them
pnpm migrate           # create the schema — applies the migrations in src/migrations
pnpm secure:db --apply # close the tables to Supabase's public API roles
pnpm bootstrap         # your settings + administrator account (it asks five questions)
pnpm seed:policies     # fill the three policy pages the footer links to
pnpm dev

Run these in this order, and do not start pnpm dev before pnpm migrate. In development Payload writes the schema itself rather than applying the migrations, and records it as a migration named dev. From then on pnpm migrate believes the schema has drifted and stops to ask whether to reset the database — with nothing at the keyboard, it waits forever and prints nothing at all. pnpm diagnose reports a database in that state, but the cheaper move is not to get there.

pnpm diagnose is first on purpose. It connects to the database, checks the bucket exists and is public, and opens a connection to your mail server, so a mistyped password or a private bucket is a sentence now rather than a puzzle three steps later. Everything it reports names what to change. Before pnpm migrate it reports the missing schema as expected rather than as a failure, so the first command in the list does not exit with an error.

pnpm seed:policies is listed last but is not decoration. The footer links to Terms, Privacy and Cookie policy on every page, and without templates those pages render a heading over nothing — a blank privacy policy reads as a claim that nothing is collected, which is worse than no page at all. The skeletons it writes still have to be edited before launch; they say so in brackets.

pnpm secure:db is the one step you cannot skip on Supabase. Supabase publishes every table in public over HTTP to the role behind the anon key — a value that ships in browsers — and Postgres creates tables with row-level security off, so a fresh install's member data is world-readable until this runs. It turns RLS on everywhere, revokes the grants, and installs an event trigger so tables added later stay closed. On plain Postgres there is nothing to close and it says so.

While you are in Supabase, turn the Data API off as well — Integrations → Data API → Enable Data API. Nothing here calls /rest/v1/; uploads go to /storage/v1/, which is a different service and keeps working. Expect the Security Advisor's Info tab to keep listing every table as “RLS Enabled No Policy” afterwards. That is the intended state, not a leftover. See ADR 0006.

Administration is the standard Payload admin panel. A user with the member-admin role can reach it; a plain member cannot.

Sharing special requests with another chapter

Two installs can show each other's special requests. An admin on each side creates a connection, hands the other a key, and members see both chapters' requests in one list with a filter in the header.

It sends the requester's contact details to the other chapter's server, so it needs an agreement between the two organisations before it needs any configuration.

→ docs/special-request-exchange.md


Deploy

Vercel + Supabase

No server to maintain: Supabase provides the database and the file storage, Vercel runs the app. About 30 minutes end to end.

→ docs/deploy-vercel-supabase.md

The app adapts itself when it detects a serverless host — a small database pool per instance, console-only logging, thumbnails held open with after() instead of detached. One limitation is the platform's rather than ours: Vercel rejects request bodies over 4.5MB, so uploads are capped well below the 15MB/50MB a normal server allows. The guide covers that and the rest of the sharp edges.

Docker

docker-compose.yml reads .env. Prepare the database first, in the same order as every other path — the app has nothing to serve until the schema exists, and the window between creating tables and closing them is not one to spend with the site already up:

docker compose run --rm migrate
docker compose run --rm secure-db
docker compose run --rm setup

Then start it:

docker compose up -d --build

These are one-off containers, not exec into the running app: the runtime image is a standalone Next.js bundle carrying neither the Payload CLI nor the TypeScript sources, so both tasks run from the builder stage instead.

Set SETUP_ORG_NAME, SETUP_ADMIN_EMAIL and SETUP_ADMIN_PASSWORD to run setup without a terminal.

Anywhere that runs Node

pnpm build && pnpm start. The build produces a standalone Next.js output.

On a serverless host other than Vercel, Netlify or Lambda — which are detected automatically — set SERVERLESS=1. It switches the same three behaviours: one database connection per instance instead of fifty, logging to the console instead of a file that the platform discards, and no memory watchdog restarting a process that has no supervisor to restart it.


What is supported

Not every combination gets the same promise, and pretending otherwise helps nobody. Three tiers, so you know before you start which part of this is guaranteed and which part is yours.

These tiers say how far the code is written and tested to hold. They are not a support queue: there is no issue tracker here and no contributions are taken (CONTRIBUTING.md). What tier 1 buys you is that the path is proven on every commit before it ships, not that someone will answer a ticket.

Tier 1 — Supabase and Vercel

Guaranteed, and proven on every commit. CI stands up an empty Postgres, applies the migrations, runs pnpm bootstrap and loads the resulting site in a browser. This is the path the code is written against and the one the install docs describe. A defect on this path is a defect in the code, not a misconfiguration on your side — which tells you where to look, not where to file.

Tier 2 — any Postgres, on Docker or bare Node

Documented, best-effort. Point POSTGRESS_DATABASE_URL anywhere; nothing outside that variable assumes Supabase for the database, and CI exercises the schema against stock Postgres rather than Supabase specifically. What is not covered is the whole combination — a report here is welcome and may take a while, because reproducing it means building your setup rather than opening a terminal.

Tier 3 — any other object storage

A seam, and no promise. Provider-specific code lives in src/lib/storage.ts: nine exports, of which all but two speak to the provider. Reimplement them for S3, R2, Bunny or a local disk and nothing else changes — the Payload adapter, the thumbnail generator and the bulk uploader all go through that module.

Nothing tests this, here or anywhere. If you take it on you are the one who knows whether it works, and a patch is more useful to both of us than a bug report.


Adapting it

Rebranding. src/lib/branding.ts holds every product name and default. Colours are theme tokens in src/app/(frontend)/styles.css — brand, ink, surface, accent — each reading a CSS custom property so they can be overridden at runtime.

Another language. Add src/messages/<code>.json alongside en.json, add the code to Locale in src/lib/i18n.ts, and register it. Messages is derived from en.json, so TypeScript will list every key you are missing. The active language is set under Settings.


The policy templates are skeletons, not legal documents. pnpm seed:policies creates Terms, Privacy and Cookie drafts with the usual sections and every substantive claim left as a [bracketed] prompt. This is deliberate: a prewritten policy naming another country's supervisory authority would look authoritative while being wrong. Fill them in and have a lawyer in your jurisdiction review them before publishing.

This software stores personal data about members — names, contact details, photos, business figures. Wherever you operate, that comes with obligations around lawful basis, retention, access and deletion. Those are yours to meet.


Development

pnpm dev              # dev server on :3050
pnpm typecheck        # tsc --noEmit
pnpm test:int         # vitest
pnpm test:e2e         # playwright
pnpm generate:types   # regenerate payload-types.ts after schema changes
pnpm migrate:create   # create a migration after changing collections

Collections live in src/collections/ and are registered in src/payload.config.ts. After changing a collection, run generate:types and migrate:create.

Access control is centralised in src/access/ — most collections use the helpers from src/access/index.ts rather than hand-rolled rules. role and status live on the User record and travel in the JWT, so an access check is a field comparison rather than a database lookup.


Licence

AGPL-3.0-or-later.

You can run, modify and self-host this freely, and fork it if you want to take it somewhere else. If you run a modified version as a network service, the AGPL requires you to offer your users the modified source.

Development being closed does not narrow any of that: the licence grants what it grants, and none of it depends on this repository accepting your changes back. If the AGPL does not suit your situation, reach the maintainer through the igstudija profile — not through issues, which are disabled.

Claim this template

More SaaS & dashboards templates

refine-project screenshot

refine-project

Payload CMS 3 and Next.js 15 starter with a Refine and Ant Design admin dashboard, in English and Arabic

3
v3
Next.js
Updated 8 months ago
copaia-shop screenshot

copaia-shop

Bilingual Hebrew and English RTL-first storefront on Next.js and Payload with a mock checkout

0
v3
Next.js
Updated 4 months ago
payload-starter screenshot

payload-starter

Next.js 16 and Payload 3 starter with PostgreSQL, role-based admin access, shadcn/ui and SEO helpers

100
v3
Next.js
Updated 4 months ago