personal activity index (bluesky, leaflet, substack)
pai.desertthunder.dev
rss
bluesky
1# Personal Activity Index – Deployment Guide
2
3This guide walks through two common reverse proxy setups for `pai serve`: **nginx** and **Caddy**.
4Both sections include native (host binary) instructions and optional Docker paths if you prefer containerized deployments.
5
6## Table of Contents
7
8- [Prerequisites](#prerequisites)
9- [nginx Deployment](#nginx-deployment)
10 - [Host Setup](#host-setup)
11 - [nginx Config](#nginx-config)
12 - [Optional: nginx via Docker](#optional-nginx-via-docker)
13- [Caddy Deployment](#caddy-deployment)
14 - [Host Setup](#host-setup-1)
15 - [Caddyfile Example](#caddyfile-example)
16 - [Optional: Caddy + Docker Compose](#optional-caddy--docker-compose)
17- [Health Checks & Monitoring](#health-checks--monitoring)
18- [Cloudflare Worker Deployment](#cloudflare-worker-deployment)
19 - [Prerequisites](#prerequisites-1)
20 - [Quick Start](#quick-start)
21 - [Cron Triggers](#cron-triggers)
22 - [API Endpoints](#api-endpoints)
23 - [Local Development](#local-development)
24 - [Monitoring](#monitoring)
25
26## Prerequisites
27
281. Build binary:
29
30 ```sh
31 cargo build --release -p pai
32 ```
33
34 The binary will live at `target/release/pai`.
35
362. 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).
373. Run a sync at least once so the database has data:
38
39 ```sh
40 ./target/release/pai sync -C /etc/pai -d /var/lib/pai/pai.db -a
41 ```
42
434. Start the server (example binds to localhost so the proxy terminates TLS):
44
45 ```sh
46 ./target/release/pai serve -d /var/lib/pai/pai.db -a 127.0.0.1:8080
47 ```
48
49### CORS Configuration for Self-Hosted Server
50
51The HTTP server supports CORS configuration via `config.toml`. Add a `[cors]` section:
52
53```toml
54[cors]
55# List of allowed origins for cross-origin requests
56allowed_origins = ["https://desertthunder.dev", "http://localhost:4321"]
57
58# Optional development key for local testing
59dev_key = "your-secret-dev-key"
60```
61
62CORS features:
63
64- **Exact matching**: `http://localhost:4321` only allows that specific origin
65- **Same-root-domain**: `https://desertthunder.dev` also allows `https://pai.desertthunder.dev`, `https://api.desertthunder.dev`, etc.
66- **Dev key**: Requests with `X-Local-Dev-Key` header matching the configured key bypass origin checks
67
68The PAI server handles CORS automatically - no additional proxy configuration needed. See [README.md](./README.md#cors-configuration) for details.
69
70## nginx Deployment
71
72### Host Setup
73
741. Install nginx via your package manager (`apt`, `dnf`, `brew`, etc.).
752. Create a systemd service for `pai` (optional but recommended):
76
77 ```ini
78 [Unit]
79 Description=Personal Activity Index
80 After=network.target
81
82 [Service]
83 ExecStart=/usr/local/bin/pai serve -d /var/lib/pai/pai.db -a 127.0.0.1:8080
84 Restart=on-failure
85 User=pai
86 Group=pai
87 WorkingDirectory=/var/lib/pai
88
89 [Install]
90 WantedBy=multi-user.target
91 ```
92
933. Enable and start it:
94
95 ```sh
96 sudo systemctl daemon-reload
97 sudo systemctl enable --now pai.service
98 ```
99
100### nginx Config
101
102Create `/etc/nginx/conf.d/pai.conf`:
103
104```nginx
105server {
106 listen 80;
107 server_name pai.example.com;
108
109 location / {
110 proxy_pass http://127.0.0.1:8080;
111 proxy_set_header Host $host;
112 proxy_set_header X-Real-IP $remote_addr;
113 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
114 proxy_set_header X-Forwarded-Proto $scheme;
115 }
116}
117```
118
119Reload nginx: `sudo nginx -s reload`.
120
121### Optional: nginx via Docker
122
123Use an `nginx` image + bind-mount config:
124
125```yaml
126services:
127 pai:
128 image: ghcr.io/your-namespace/pai:latest
129 command: ["serve", "-d", "/data/pai.db", "-a", "0.0.0.0:8080"]
130 volumes:
131 - ./data:/data
132 expose:
133 - "8080"
134
135 nginx:
136 image: nginx:1.27
137 volumes:
138 - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
139 ports:
140 - "80:80"
141 depends_on:
142 - pai
143```
144
145`nginx.conf` should proxy to `http://pai:8080` instead of localhost.
146
147## Caddy Deployment
148
149### Host Setup
150
1511. Install Caddy (<https://caddyserver.com/docs/install>).
1522. Keep the same `pai` systemd service from above (or run manually).
153
154### Caddyfile Example
155
156Create `/etc/caddy/Caddyfile`:
157
158```caddyfile
159pai.example.com {
160 reverse_proxy 127.0.0.1:8080
161 encode gzip zstd
162 header {
163 Referrer-Policy "no-referrer-when-downgrade"
164 X-Content-Type-Options "nosniff"
165 }
166}
167```
168
169Caddy automatically provisions TLS certificates with Let’s Encrypt. Reload with `sudo systemctl reload caddy`.
170
171### Optional: Caddy + Docker Compose
172
173```yaml
174services:
175 pai:
176 image: ghcr.io/your-namespace/pai:latest
177 command: ["serve", "-d", "/data/pai.db", "-a", "0.0.0.0:8080"]
178 volumes:
179 - ./data:/data
180 expose:
181 - "8080"
182
183 caddy:
184 image: caddy:2
185 volumes:
186 - ./Caddyfile:/etc/caddy/Caddyfile:ro
187 - caddy_data:/data
188 - caddy_config:/config
189 ports:
190 - "80:80"
191 - "443:443"
192 depends_on:
193 - pai
194
195volumes:
196 caddy_data:
197 caddy_config:
198```
199
200Use the same `Caddyfile` contents as above, but point `reverse_proxy` to `pai:8080`.
201
202## Health Checks & Monitoring
203
204- `GET /status` – lightweight JSON (`status`, version, uptime, total items, counts per `source_kind`). Ideal for load balancer health probes.
205- `GET /api/feed?limit=1` ensures the server can read from SQLite and return real data.
206- `GET /api/item/{id}` is handy for debugging a specific record.
207- Consider wiring `/status` into nginx/Caddy health checks (`/healthz`) or your platform’s monitoring agents.
208
209## Cloudflare Worker Deployment
210
211The Personal Activity Index can also be deployed as a Cloudflare Worker with D1 database, providing a serverless alternative to self-hosting.
212
213### Prerequisites
214
2151. Cloudflare account with Workers enabled
2162. [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/) installed
217 `npx wrangler` works here as well.
2183. Rust toolchain with `wasm32-unknown-unknown` target
2194. Crate `worker-build`
220
221### Quick Start
222
223#### 1. Generate Scaffolding
224
225Use the `pai cf-init` command to generate Cloudflare Worker configuration:
226
227```sh
228# Dry run to preview files
229pai cf-init --dry-run -o cloudflare-deployment
230
231# Create scaffolding
232pai cf-init -o cloudflare-deployment
233cd cloudflare-deployment
234```
235
236This creates:
237
238- `wrangler.example.toml` - Worker configuration template
239- `schema.sql` - D1 database schema
240- `README.md` - Deployment instructions
241
242#### 2. Create D1 Database
243
244```sh
245wrangler d1 create personal-activity-db
246```
247
248Copy the database ID from the output and update `wrangler.example.toml`:
249
250```toml
251[[d1_databases]]
252binding = "DB"
253database_name = "personal-activity-db"
254database_id = "your-database-id-here" # Replace with returned database_id
255```
256
257Then copy to the active config:
258
259```sh
260cp wrangler.example.toml wrangler.toml
261```
262
263#### 3. Initialize Database Schema
264
265```sh
266wrangler d1 execute personal-activity-db --remote --file=schema.sql
267```
268
269Note that you can omit `--remote` for local development.
270
271#### 4. Build and Deploy
272
273```sh
274# Build the worker
275cd ..
276cargo install worker-build
277worker-build --release worker
278```
279
280#### 5. Patch Generated Code
281
282The worker-build output requires two patches for compatibility with wrangler:
283
284```sh
285# 1. Fix import syntax (remove 'source' keyword)
286sed -i.bak 's/import source wasmModule/import wasmModule/' worker/build/index.js
287
288# 2. Add default export for ES module format (required for D1 bindings)
289echo -e "\nexport default { fetch, scheduled };" >> worker/build/index.js
290```
291
292On macOS, use `sed -i '' ...` instead of `sed -i.bak ...`.
293
294#### 6. Deploy
295
296```sh
297cd cloudflare-deployment
298wrangler deploy
299```
300
301### Cron Triggers
302
303The worker includes a scheduled event handler for automatic syncing. Configure the schedule in `wrangler.toml`:
304
305```toml
306[triggers]
307crons = ["0 * * * *"] # Every hour at minute 0
308```
309
310Common schedules:
311
312- `*/30 * * * *` - Every 30 minutes
313- `0 */6 * * *` - Every 6 hours
314- `0 0 * * *` - Daily at midnight
315
316### Environment Variables
317
318Configure sources in `wrangler.toml` under `[vars]`:
319
320```toml
321[vars]
322# Substack RSS feed URL
323SUBSTACK_URL = "https://patternmatched.substack.com"
324
325# Bluesky handle
326BLUESKY_HANDLE = "desertthunder.dev"
327
328# Leaflet publications (comma-separated id:url pairs)
329LEAFLET_URLS = "desertthunder:https://desertthunder.leaflet.pub,stormlightlabs:https://stormlightlabs.leaflet.pub"
330
331# BearBlog publications (comma-separated id:url pairs)
332BEARBLOG_URLS = "desertthunder:https://desertthunder.bearblog.dev"
333
334# CORS configuration (optional)
335CORS_ALLOWED_ORIGINS = "https://desertthunder.dev,http://localhost:4321"
336CORS_DEV_KEY = "your-secret-dev-key"
337```
338
339### CORS Configuration
340
341The Worker supports CORS to allow cross-origin requests from your web applications.
342
343#### Environment Variables
344
345Add to `wrangler.toml` under `[vars]`:
346
347- **CORS_ALLOWED_ORIGINS**: Comma-separated list of allowed origins
348 - Supports exact matching: `http://localhost:4321` only allows that exact origin
349 - Supports same-root-domain: `https://desertthunder.dev` also allows `https://pai.desertthunder.dev`, `https://api.desertthunder.dev`, etc.
350
351- **CORS_DEV_KEY**: Optional development key for local testing
352 - When set, requests with the `X-Local-Dev-Key` header matching this value bypass origin checking
353 - Useful for testing from different local ports during development
354
355#### Example Configuration
356
357```toml
358[vars]
359# Allow requests from your main domain and localhost for development
360CORS_ALLOWED_ORIGINS = "https://desertthunder.dev,http://localhost:4321"
361
362# Dev key for local Astro development
363CORS_DEV_KEY = "local-dev-secret-123"
364```
365
366#### Usage from JavaScript
367
368```javascript
369// Production request from https://desertthunder.dev
370fetch('https://pai.desertthunder.dev/api/feed', {
371 credentials: 'include'
372})
373
374// Development request from http://localhost:4321
375fetch('http://localhost:8787/api/feed', {
376 headers: {
377 'X-Local-Dev-Key': 'local-dev-secret-123'
378 }
379})
380```
381
382#### Same-Root-Domain Support
383
384When you configure `CORS_ALLOWED_ORIGINS = "https://desertthunder.dev"`:
385
386- ✓ `https://desertthunder.dev` (exact match)
387- ✓ `https://pai.desertthunder.dev` (subdomain)
388- ✓ `https://api.desertthunder.dev` (subdomain)
389- ✗ `https://evil.dev` (different root domain)
390
391This 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.
392
393### API Endpoints
394
395The Worker exposes the following API:
396
397- `GET /` - API documentation (JSON)
398- `GET /api/feed?source_kind=bluesky&limit=20` - List items with optional filters
399- `GET /api/item/{id}` - Get single item by ID
400- `POST /api/sync` - Manually trigger synchronization from all configured sources
401- `GET /status` - Health check and version info
402
403### Local Development
404
405Test the worker locally before deploying:
406
407```sh
408wrangler dev
409```
410
411This starts a local server at `http://localhost:8787` with live reload.
412
413### Monitoring
414
415View logs in real-time:
416
417```sh
418wrangler tail
419```
420
421Or check logs in the [Cloudflare Dashboard](https://dash.cloudflare.com) under Workers & Pages.