diff --git a/README.md b/README.md index 51fc18e5..7db24e0c 100644 --- a/README.md +++ b/README.md @@ -23,16 +23,13 @@ gnubok implements double-entry bookkeeping compliant with Swedish accounting law ## Self-Hosting ```bash -# Clone and configure git clone https://github.com/erp-mafia/gnubok.git cd gnubok -cp .env.example .env # Fill in your Supabase credentials - -# Start with Docker Compose +cp .env.docker.example .env # Fill in your Supabase credentials docker compose up -d ``` -See `docker-compose.yml` for the full service configuration. You need a Supabase project -- create one at [supabase.com](https://supabase.com) and run the migrations from `supabase/migrations/`. +You need a Supabase project and must apply the database migrations before first use. See [SELF-HOSTING.md](SELF-HOSTING.md) for the full step-by-step guide, including Supabase setup, auth configuration, optional features (AI, email, push notifications), and troubleshooting. ## Development Setup @@ -55,6 +52,7 @@ npm run lint # ESLint ## Documentation +- [SELF-HOSTING.md](SELF-HOSTING.md) -- Full self-hosting guide (Docker, Supabase setup, migrations, optional features) - [CLAUDE.md](CLAUDE.md) -- Architecture, bookkeeping engine, database conventions, extension system - [CONTRIBUTING.md](CONTRIBUTING.md) -- Development workflow, code style, pull request process - [SECURITY.md](SECURITY.md) -- Vulnerability reporting policy diff --git a/SELF-HOSTING.md b/SELF-HOSTING.md new file mode 100644 index 00000000..942df900 --- /dev/null +++ b/SELF-HOSTING.md @@ -0,0 +1,269 @@ +# Self-Hosting gnubok + +This guide walks you through deploying gnubok on your own infrastructure using Docker. + +## Prerequisites + +- Docker and Docker Compose v2+ +- A Supabase project (free tier works — create one at [supabase.com](https://supabase.com)) + +## 1. Create a Supabase Project + +1. Go to [supabase.com](https://supabase.com) and create a new project. +2. Note these values from **Settings > API**: + - `Project URL` (e.g., `https://abcdefgh.supabase.co`) + - `anon` public key + - `service_role` secret key + +## 2. Configure Supabase Auth + +In the Supabase dashboard under **Authentication > URL Configuration**: + +1. Set **Site URL** to your deployment URL (e.g., `https://gnubok.example.com`). +2. Add `https://gnubok.example.com/auth/callback` to the **Redirect URLs** allowlist. + +gnubok uses passwordless magic link login — the default Supabase email auth settings work out of the box. For production, configure a custom SMTP provider under **Authentication > SMTP Settings** to avoid Supabase's built-in rate limits. + +## 3. Apply Database Migrations + +The `supabase/migrations/` directory contains 52 ordered SQL files that set up the full schema, including tables, RLS policies, triggers, and functions. + +**Option A — Supabase CLI (recommended):** + +```bash +# Install the Supabase CLI +npm install -g supabase + +# Link to your project +supabase link --project-ref + +# Push all migrations +supabase db push +``` + +**Option B — SQL Editor:** + +Run each file in `supabase/migrations/` in order (001 through 052) in the Supabase SQL Editor. They must be applied sequentially — later migrations depend on earlier ones. + +### PostgreSQL Extensions + +The migrations automatically enable these extensions: + +| Extension | Migration | Purpose | +|-----------|-----------|---------| +| `uuid-ossp` | 001 | UUID generation | +| `vector` (pgvector) | 033 | AI embedding storage (for AI extensions) | +| `btree_gist` | 042 | Fiscal period overlap prevention | +| `pg_cron` | 048 | In-database scheduled jobs | + +These are all available on Supabase hosted. `pg_cron` requires a paid plan — if you are on the free tier, migration 048 will fail. You can safely skip it; the cron sidecar container handles the equivalent job via HTTP instead. + +## 4. Configure Environment + +```bash +git clone https://github.com/erp-mafia/gnubok.git +cd gnubok +cp .env.docker.example .env +``` + +Edit `.env` with your values: + +```bash +# ─── Required ─── +NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key +SUPABASE_SERVICE_ROLE_KEY=your-service-role-key +NEXT_PUBLIC_APP_URL=https://your-domain.com +CRON_SECRET= +``` + +`NEXT_PUBLIC_APP_URL` must match your public-facing URL. It is used in invoice reminder emails, calendar feed links, and PSD2 callbacks. If left as a placeholder, links will be broken. + +## 5. Start the Application + +```bash +docker compose up -d +``` + +This starts two containers: + +| Container | Purpose | +|-----------|---------| +| `app` | Next.js application (pulls `ghcr.io/erp-mafia/gnubok:latest`) | +| `cron` | Scheduled jobs via [supercronic](https://github.com/aptible/supercronic) | + +The cron container waits for the app health check to pass before starting. + +Verify the deployment: + +```bash +curl http://localhost:3000/api/health +# {"status":"healthy","timestamp":"...","version":"1.0.0"} +``` + +> **Note:** The health check queries the database, so migrations must be applied before it returns healthy. + +### Building from Source + +To build the Docker image locally instead of pulling from GHCR: + +```bash +docker compose -f docker-compose.yml -f docker-compose.build.yml up --build +``` + +### Custom Port + +Set `PORT` in your `.env` or environment to change the host port (the container always listens on 3000 internally): + +```bash +PORT=8080 docker compose up -d +``` + +## 6. First Login + +1. Open your deployment URL in a browser. +2. Enter any email address and click "Skicka magisk lank" (Send magic link). +3. Check your email and click the link. +4. Complete the 5-step onboarding wizard: + - **Step 1**: Choose entity type (enskild firma or aktiebolag) + - **Step 2**: Company name and org number + - **Step 3**: Fiscal year, VAT registration, accounting method + - **Step 4**: Preliminary tax amount (optional, skip if unsure) + - **Step 5**: Bank details for invoices (optional) + +There is no admin account or invite system — any email address that receives the magic link can sign up. + +## Scheduled Jobs + +The cron sidecar runs these jobs automatically: + +| Schedule (UTC) | Endpoint | Purpose | +|----------------|----------|---------| +| Daily 06:00 | `/api/deadlines/status/cron` | Update deadline statuses | +| Daily 08:00 | `/api/invoices/reminders/cron` | Send overdue invoice reminders | +| Yearly Jan 2 | `/api/tax-deadlines/cron` | Generate tax deadlines for the new year | +| Sundays 03:00 | `/api/documents/verify/cron` | SHA-256 integrity check on document archive | + +All cron endpoints are authenticated with `Authorization: Bearer `. The cron container calls the app over the internal Docker network (`http://app:3000`), so these endpoints are not exposed publicly. + +Additionally, migration 048 schedules a `pg_cron` job inside the database that marks overdue supplier invoices daily at 06:00 UTC. + +## Optional Features + +### AI Features + +The self-hosted Docker image includes these AI-powered extensions: receipt OCR, AI categorization, AI chat, and invoice inbox. To enable them, add API keys to your `.env`: + +```bash +ANTHROPIC_API_KEY=sk-ant-... # Required for all AI features +OPENAI_API_KEY=sk-... # Required for embedding-based features (categorization, chat) +``` + +Each user must individually grant AI consent in the UI before AI features activate (per GDPR requirements). + +**AI chat knowledge base** (optional): The AI chat can answer Swedish tax and accounting questions using a RAG knowledge base. To populate it, create `dev_docs/ai_knowledge_base/` with markdown files and run: + +```bash +npx tsx extensions/general/ai-chat/ingestion/ingest.ts +``` + +**Booking template embeddings** (optional): For AI-powered transaction categorization suggestions, seed the template embeddings by calling: + +```bash +curl -X POST -H "Authorization: Bearer $CRON_SECRET" \ + https://your-domain.com/api/admin/seed-template-embeddings +``` + +### Email (Invoice Sending and Reminders) + +```bash +RESEND_API_KEY=re_... +RESEND_FROM_EMAIL=noreply@your-domain.com +``` + +Requires a [Resend](https://resend.com) account with a verified sender domain. Without this, invoices can still be generated as PDFs but cannot be emailed. + +### Push Notifications + +```bash +NEXT_PUBLIC_VAPID_PUBLIC_KEY=... +VAPID_PRIVATE_KEY=... +VAPID_SUBJECT=mailto:you@example.com +``` + +Generate VAPID keys with `npx web-push generate-vapid-keys`. Push notifications require HTTPS. + +### Error Tracking (Sentry) + +```bash +SENTRY_DSN=https://...@sentry.io/... +NEXT_PUBLIC_SENTRY_DSN=https://...@sentry.io/... +``` + +Sentry is disabled if these are not set. No errors are thrown. + +## Storage Buckets + +Migration 024 automatically creates the `documents` storage bucket (private, 50 MB limit, WORM — no update/delete). + +If you enable the **receipt-ocr** extension, you must manually create a `receipts` storage bucket in the Supabase dashboard: + +1. Go to **Storage** in the Supabase dashboard. +2. Create a new bucket named `receipts`. +3. Set it as **public** (receipt images are referenced by public URL). +4. Set an appropriate file size limit (e.g., 10 MB). + +## Updating + +Pull the latest image and restart: + +```bash +docker compose pull +docker compose up -d +``` + +If a new release includes database migrations, apply them before restarting: + +```bash +supabase db push +``` + +Check the [release notes](https://github.com/erp-mafia/gnubok/releases) for migration instructions. + +## Architecture Overview + +``` +┌─────────────────────┐ ┌──────────────────┐ +│ Docker: app │ │ Docker: cron │ +│ (Next.js) │◄────│ (supercronic) │ +│ Port 3000 │ │ Bearer auth │ +└────────┬────────────┘ └──────────────────┘ + │ + │ HTTPS + ▼ +┌─────────────────────┐ +│ Supabase │ +│ - PostgreSQL + RLS │ +│ - Auth (magic link)│ +│ - Storage (docs) │ +└─────────────────────┘ +``` + +The Next.js app is stateless — all data lives in Supabase. The Docker entrypoint injects your `NEXT_PUBLIC_*` environment variables into the pre-built JS bundles at container startup, so a single image works with any Supabase project. + +## Troubleshooting + +**Health check fails with "unhealthy":** +Migrations have not been applied, or the Supabase credentials are wrong. Check that `NEXT_PUBLIC_SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` are correct and that migrations have been pushed. + +**Magic link email not arriving:** +Check the Supabase dashboard under **Authentication > Users** to verify the signup attempt was received. On the free tier, Supabase rate-limits emails to 4/hour. Configure custom SMTP under **Authentication > SMTP Settings** for production use. + +**Auth callback redirects to error:** +Ensure `https://your-domain.com/auth/callback` is in the Supabase **Redirect URLs** allowlist and that **Site URL** matches your `NEXT_PUBLIC_APP_URL`. + +**`pg_cron` migration fails:** +`pg_cron` requires a paid Supabase plan. On the free tier, you can safely comment out migration 048 or let it fail — the overdue supplier invoice check is non-critical and can be triggered manually. + +**Container restarts in a loop:** +Check logs with `docker compose logs app`. The app requires all five core env vars (`NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`, `NEXT_PUBLIC_APP_URL`, `CRON_SECRET`) and will crash on startup if any are missing.