# Personal Activity Index – Deployment Guide This guide walks through two common reverse proxy setups for `pai serve`: **nginx** and **Caddy**. Both sections include native (host binary) instructions and optional Docker paths if you prefer containerized deployments. ## Table of Contents - [Prerequisites](#prerequisites) - [nginx Deployment](#nginx-deployment) - [Host Setup](#host-setup) - [nginx Config](#nginx-config) - [Optional: nginx via Docker](#optional-nginx-via-docker) - [Caddy Deployment](#caddy-deployment) - [Host Setup](#host-setup-1) - [Caddyfile Example](#caddyfile-example) - [Optional: Caddy + Docker Compose](#optional-caddy--docker-compose) - [Health Checks & Monitoring](#health-checks--monitoring) - [Cloudflare Worker Deployment](#cloudflare-worker-deployment) - [Prerequisites](#prerequisites-1) - [Quick Start](#quick-start) - [Cron Triggers](#cron-triggers) - [API Endpoints](#api-endpoints) - [Local Development](#local-development) - [Monitoring](#monitoring) ## Prerequisites 1. Build binary: ```sh cargo build --release -p pai ``` The binary will live at `target/release/pai`. 2. Prepare a configuration + database location. The default locations follow the XDG spec, but you can override them with `-C` (config dir) and `-d` (database path). 3. Run a sync at least once so the database has data: ```sh ./target/release/pai sync -C /etc/pai -d /var/lib/pai/pai.db -a ``` 4. Start the server (example binds to localhost so the proxy terminates TLS): ```sh ./target/release/pai serve -d /var/lib/pai/pai.db -a 127.0.0.1:8080 ``` ### CORS Configuration for Self-Hosted Server The HTTP server supports CORS configuration via `config.toml`. Add a `[cors]` section: ```toml [cors] # List of allowed origins for cross-origin requests allowed_origins = ["https://desertthunder.dev", "http://localhost:4321"] # Optional development key for local testing dev_key = "your-secret-dev-key" ``` CORS features: - **Exact matching**: `http://localhost:4321` only allows that specific origin - **Same-root-domain**: `https://desertthunder.dev` also allows `https://pai.desertthunder.dev`, `https://api.desertthunder.dev`, etc. - **Dev key**: Requests with `X-Local-Dev-Key` header matching the configured key bypass origin checks The PAI server handles CORS automatically - no additional proxy configuration needed. See [README.md](./README.md#cors-configuration) for details. ## nginx Deployment ### Host Setup 1. Install nginx via your package manager (`apt`, `dnf`, `brew`, etc.). 2. Create a systemd service for `pai` (optional but recommended): ```ini [Unit] Description=Personal Activity Index After=network.target [Service] ExecStart=/usr/local/bin/pai serve -d /var/lib/pai/pai.db -a 127.0.0.1:8080 Restart=on-failure User=pai Group=pai WorkingDirectory=/var/lib/pai [Install] WantedBy=multi-user.target ``` 3. Enable and start it: ```sh sudo systemctl daemon-reload sudo systemctl enable --now pai.service ``` ### nginx Config Create `/etc/nginx/conf.d/pai.conf`: ```nginx server { listen 80; server_name pai.example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` Reload nginx: `sudo nginx -s reload`. ### Optional: nginx via Docker Use an `nginx` image + bind-mount config: ```yaml services: pai: image: ghcr.io/your-namespace/pai:latest command: ["serve", "-d", "/data/pai.db", "-a", "0.0.0.0:8080"] volumes: - ./data:/data expose: - "8080" nginx: image: nginx:1.27 volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro ports: - "80:80" depends_on: - pai ``` `nginx.conf` should proxy to `http://pai:8080` instead of localhost. ## Caddy Deployment ### Host Setup 1. Install Caddy (). 2. Keep the same `pai` systemd service from above (or run manually). ### Caddyfile Example Create `/etc/caddy/Caddyfile`: ```caddyfile pai.example.com { reverse_proxy 127.0.0.1:8080 encode gzip zstd header { Referrer-Policy "no-referrer-when-downgrade" X-Content-Type-Options "nosniff" } } ``` Caddy automatically provisions TLS certificates with Let’s Encrypt. Reload with `sudo systemctl reload caddy`. ### Optional: Caddy + Docker Compose ```yaml services: pai: image: ghcr.io/your-namespace/pai:latest command: ["serve", "-d", "/data/pai.db", "-a", "0.0.0.0:8080"] volumes: - ./data:/data expose: - "8080" caddy: image: caddy:2 volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config ports: - "80:80" - "443:443" depends_on: - pai volumes: caddy_data: caddy_config: ``` Use the same `Caddyfile` contents as above, but point `reverse_proxy` to `pai:8080`. ## Health Checks & Monitoring - `GET /status` – lightweight JSON (`status`, version, uptime, total items, counts per `source_kind`). Ideal for load balancer health probes. - `GET /api/feed?limit=1` ensures the server can read from SQLite and return real data. - `GET /api/item/{id}` is handy for debugging a specific record. - Consider wiring `/status` into nginx/Caddy health checks (`/healthz`) or your platform’s monitoring agents. ## Cloudflare Worker Deployment The Personal Activity Index can also be deployed as a Cloudflare Worker with D1 database, providing a serverless alternative to self-hosting. ### Prerequisites 1. Cloudflare account with Workers enabled 2. [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/) installed `npx wrangler` works here as well. 3. Rust toolchain with `wasm32-unknown-unknown` target 4. Crate `worker-build` ### Quick Start #### 1. Generate Scaffolding Use the `pai cf-init` command to generate Cloudflare Worker configuration: ```sh # Dry run to preview files pai cf-init --dry-run -o cloudflare-deployment # Create scaffolding pai cf-init -o cloudflare-deployment cd cloudflare-deployment ``` This creates: - `wrangler.example.toml` - Worker configuration template - `schema.sql` - D1 database schema - `README.md` - Deployment instructions #### 2. Create D1 Database ```sh wrangler d1 create personal-activity-db ``` Copy the database ID from the output and update `wrangler.example.toml`: ```toml [[d1_databases]] binding = "DB" database_name = "personal-activity-db" database_id = "your-database-id-here" # Replace with returned database_id ``` Then copy to the active config: ```sh cp wrangler.example.toml wrangler.toml ``` #### 3. Initialize Database Schema ```sh wrangler d1 execute personal-activity-db --remote --file=schema.sql ``` Note that you can omit `--remote` for local development. #### 4. Build and Deploy ```sh # Build the worker cd .. cargo install worker-build worker-build --release worker ``` #### 5. Patch Generated Code The worker-build output requires two patches for compatibility with wrangler: ```sh # 1. Fix import syntax (remove 'source' keyword) sed -i.bak 's/import source wasmModule/import wasmModule/' worker/build/index.js # 2. Add default export for ES module format (required for D1 bindings) echo -e "\nexport default { fetch, scheduled };" >> worker/build/index.js ``` On macOS, use `sed -i '' ...` instead of `sed -i.bak ...`. #### 6. Deploy ```sh cd cloudflare-deployment wrangler deploy ``` ### Cron Triggers The worker includes a scheduled event handler for automatic syncing. Configure the schedule in `wrangler.toml`: ```toml [triggers] crons = ["0 * * * *"] # Every hour at minute 0 ``` Common schedules: - `*/30 * * * *` - Every 30 minutes - `0 */6 * * *` - Every 6 hours - `0 0 * * *` - Daily at midnight ### Environment Variables Configure sources in `wrangler.toml` under `[vars]`: ```toml [vars] # Substack RSS feed URL SUBSTACK_URL = "https://patternmatched.substack.com" # Bluesky handle BLUESKY_HANDLE = "desertthunder.dev" # Leaflet publications (comma-separated id:url pairs) LEAFLET_URLS = "desertthunder:https://desertthunder.leaflet.pub,stormlightlabs:https://stormlightlabs.leaflet.pub" # BearBlog publications (comma-separated id:url pairs) BEARBLOG_URLS = "desertthunder:https://desertthunder.bearblog.dev" # CORS configuration (optional) CORS_ALLOWED_ORIGINS = "https://desertthunder.dev,http://localhost:4321" CORS_DEV_KEY = "your-secret-dev-key" ``` ### CORS Configuration The Worker supports CORS to allow cross-origin requests from your web applications. #### Environment Variables Add to `wrangler.toml` under `[vars]`: - **CORS_ALLOWED_ORIGINS**: Comma-separated list of allowed origins - Supports exact matching: `http://localhost:4321` only allows that exact origin - Supports same-root-domain: `https://desertthunder.dev` also allows `https://pai.desertthunder.dev`, `https://api.desertthunder.dev`, etc. - **CORS_DEV_KEY**: Optional development key for local testing - When set, requests with the `X-Local-Dev-Key` header matching this value bypass origin checking - Useful for testing from different local ports during development #### Example Configuration ```toml [vars] # Allow requests from your main domain and localhost for development CORS_ALLOWED_ORIGINS = "https://desertthunder.dev,http://localhost:4321" # Dev key for local Astro development CORS_DEV_KEY = "local-dev-secret-123" ``` #### Usage from JavaScript ```javascript // Production request from https://desertthunder.dev fetch('https://pai.desertthunder.dev/api/feed', { credentials: 'include' }) // Development request from http://localhost:4321 fetch('http://localhost:8787/api/feed', { headers: { 'X-Local-Dev-Key': 'local-dev-secret-123' } }) ``` #### Same-Root-Domain Support When you configure `CORS_ALLOWED_ORIGINS = "https://desertthunder.dev"`: - ✓ `https://desertthunder.dev` (exact match) - ✓ `https://pai.desertthunder.dev` (subdomain) - ✓ `https://api.desertthunder.dev` (subdomain) - ✗ `https://evil.dev` (different root domain) This allows you to deploy the Worker to `pai.desertthunder.dev` and access it from your main site at `desertthunder.dev` without explicitly listing every subdomain. ### API Endpoints The Worker exposes the following API: - `GET /` - API documentation (JSON) - `GET /api/feed?source_kind=bluesky&limit=20` - List items with optional filters - `GET /api/item/{id}` - Get single item by ID - `POST /api/sync` - Manually trigger synchronization from all configured sources - `GET /status` - Health check and version info ### Local Development Test the worker locally before deploying: ```sh wrangler dev ``` This starts a local server at `http://localhost:8787` with live reload. ### Monitoring View logs in real-time: ```sh wrangler tail ``` Or check logs in the [Cloudflare Dashboard](https://dash.cloudflare.com) under Workers & Pages.