1What you need
- Node.js 20+ (or Bun 1.1+) and Git on the build machine.
- A Postgres database with the Supabase stack (Auth + PostgREST). Either Supabase Cloud or a self-hosted Supabase deployment via
docker compose. - A host for the web app: any Node server, container platform, or an edge runtime such as Cloudflare Workers.
- An SMTP provider if you want password resets and email notices.
2Get the code
git clone <your-fork-url> civicledger cd civicledger npm install # or: bun install
3Stand up the database
For a fully self-hosted backend, run the official Supabase Docker bundle, which brings Postgres, GoTrue (auth), PostgREST (the data API) and Storage:
git clone --depth 1 https://github.com/supabase/supabase cd supabase/docker cp .env.example .env # set POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY docker compose up -d
Then apply the CivicLedger schema. Every migration lives in supabase/migrations and is plain SQL, applied in filename order:
npx supabase link --project-ref <ref> # cloud npx supabase db push # or, against any Postgres URL: for f in supabase/migrations/*.sql; do psql "$DATABASE_URL" -f "$f"; done
The migrations create the bodies, seats, item types, numbering rules, vote rules, legislative files, meetings, agendas, motions, votes, minutes and audit tables, plus the row-level-security policies and the assign_item_number numbering function.
4Configure environment variables
Browser-visible values are prefixed VITE_. Everything else is server-only and must never be exposed to the client.
# .env VITE_SUPABASE_URL=https://db.your-domain.gov VITE_SUPABASE_PUBLISHABLE_KEY=<anon/publishable key> SUPABASE_URL=https://db.your-domain.gov SUPABASE_PUBLISHABLE_KEY=<anon/publishable key> SUPABASE_SERVICE_ROLE_KEY=<service role key — server only>
The service-role key bypasses row-level security. Keep it in your host's secret store, never in the repository or in any VITE_ variable.
5Build and run
npm run dev # local development on http://localhost:8080 npm run build # production build node .output/server/index.mjs # serve the build
The build output is a standard server bundle plus static assets. To deploy to Cloudflare Workers instead, keep the default Nitro Cloudflare preset and publish with wrangler deploy. Put the app behind TLS and your municipality's domain.
6Create the first administrator
Register through the staff sign-in page, then grant yourself the admin role directly in the database — roles are stored in a dedicated user_roles table, never on the profile, so they cannot be escalated from the client:
insert into public.user_roles (user_id, role) select id, 'admin' from auth.users where email = 'clerk@your-city.gov';
After that, every other role (clerk, attorney, department staff, board member, viewer) is assigned from the administration portal.
7Configure your municipality
Sign in and work through Administration in order:
- Municipality settings — name, clerk contact, notice days, posting deadline.
- Bodies — council, commissions and boards, with seat counts and quorum rules.
- Members and seats — who occupies each seat, for roll call and voting.
- Item types — ordinance, resolution, proclamation; readings and hearing rules.
- Vote rules — fraction, basis, abstention handling, recusal treatment.
- Numbering — patterns such as
{YYYY}-{seq:000}and reset policy.
8Backups and operations
- Nightly logical backups:
pg_dump --format=custom, retained off-site for at least as long as your records-retention schedule requires. - Generated agenda packets and minutes are versioned; every download is logged in
document_exportswith its version and filename for the official record. - The
audit_logtable records number assignments and workflow actions — never truncate it. - Upgrades: pull, run new migrations, rebuild, restart. Migrations are additive and ordered.
9Licensing and support
Self-hosted instances receive no warranty. Before adopting CivicLedger as the system of record, confirm with your attorney that the retention, notice and open-meeting requirements of your jurisdiction are satisfied by your configuration.