11# Underlay
22
3- A versioned, content-addressed registry for knowledge collections. Apps publish snapshots of their data to Underlay; Underlay preserves them, deduplicates files, and exposes them via a stable HTTPS API.
4-
5- ** Underlay is the archive underneath your app.**
3+ A versioned, content-addressed registry for structured knowledge. Apps publish snapshots of their data to Underlay; Underlay preserves them, deduplicates files, and exposes them via a stable HTTPS API.
64
75Built by [ Knowledge Futures] ( https://www.knowledgefutures.org ) , a 501(c)(3) nonprofit.
86
@@ -16,181 +14,137 @@ Built by [Knowledge Futures](https://www.knowledgefutures.org), a 501(c)(3) nonp
1614### Development
1715
1816``` bash
19- # Clone and start everything (Postgres, MinIO, app)
2017git clone https://github.com/knowledgefutures/underlay.git
2118cd underlay
2219./dev.sh
2320```
2421
2522This starts:
26- - ** PostgreSQL 16** on port 5432
23+ - ** PostgreSQL 16** on port 5433 (host) → 5432 (container)
2724- ** MinIO** (S3-compatible storage) on ports 9000/9001
28- - ** Underlay** on port 4321 (frontend ) and port 3000 (API)
25+ - ** Underlay** on port 4321 (Astro SSR ) and port 3000 (Fastify API)
2926
30- The dev script auto-creates ` .env.local ` from defaults if one doesn't exist.
27+ The dev script auto-creates ` .env.local ` from ` .env.test ` defaults if one doesn't exist.
3128
3229### Without Docker
3330
3431``` bash
3532npm install
36-
37- # Set up your own Postgres and S3, then:
3833cp .env.test .env.local
39- # Edit .env.local with your connection strings
40-
34+ # Edit .env.local with your Postgres and S3 connection strings
4135npm run db:migrate
4236npm run db:seed
4337npm run dev:server
4438```
4539
4640### Default Seed User
4741
48- The seed creates an admin account you can log in with:
4942- ** Email:** admin@underlay.org
5043- ** Password:** admin
5144
52- It also creates a "Knowledge Futures" org with three sample collections.
45+ Also creates a "Knowledge Futures" org with sample collections.
5346
5447## Architecture
5548
5649| Layer | Technology |
5750| -------| -----------|
5851| Frontend | Astro 6 SSR + React 19 islands + Tailwind CSS 4 |
59- | API | Fastify 5 (TypeScript) |
52+ | API | Fastify 5 (TypeScript), always binds to port 3000 |
6053| Database | PostgreSQL 16 + Drizzle ORM |
61- | File Storage | S3-compatible (AWS S3 / MinIO / R2) |
54+ | File Storage | Cloudflare R2 (prod) / MinIO (dev) — S3-compatible |
6255| Auth | Session cookies (web) + API keys (programmatic) |
63- | Deployment | Docker (Alpine), multi-stage build |
56+ | Deployment | Docker Swarm on Hetzner, Caddy reverse proxy, Cloudflare DNS |
57+ | CI/CD | GitHub Actions → GHCR → SSH → ` docker stack deploy ` |
58+ | Secrets | SOPS + age encryption |
6459
6560## Project Structure
6661
6762```
6863src/
69- ├── api/ # Fastify API server
70- │ ├── plugins/auth.ts # Authentication (API keys + sessions )
71- │ ├── routes/ # API route handlers
72- │ │ ├── accounts.ts # Signup, login, API key CRUD
64+ ├── api/ # Fastify API server (port 3000)
65+ │ ├── plugins/auth.ts # Auth (API keys + session cookies )
66+ │ ├── routes/
67+ │ │ ├── accounts.ts # Signup, login, API key CRUD, orgs
7368│ │ ├── collections.ts # Collection CRUD
74- │ │ ├── versions.ts # Version push/pull/diff
69+ │ │ ├── versions.ts # Version push/pull/diff + privacy filtering
7570│ │ ├── files.ts # Content-addressed file storage
7671│ │ └── health.ts # Health check
7772│ └── server.ts # Fastify entry point
7873├── db/
7974│ ├── schema.ts # Drizzle table definitions
8075│ ├── index.ts # Database client
81- │ ├── migrate.ts # Migration runner
82- │ ├── seed.ts # Seed data (--force to re-seed)
76+ │ ├── migrate.ts # Migration runner (retries on DNS failures)
77+ │ ├── seed.ts # Seed data
8378│ └── migrations/ # Generated SQL migrations
84- ├── layouts/
85- │ ├── Base.astro # Root HTML layout
86- │ └── Docs.astro # Documentation page layout
79+ ├── layouts/ # Astro layouts (Base, Docs, BlogPost)
80+ ├── components/ # React islands + Astro components
8781├── lib/
88- │ └── s3.ts # S3 client utilities
82+ │ ├── s3.ts # S3 client (upload, download, head, list, delete)
83+ │ └── page-utils.ts # SSR utilities
8984├── pages/
9085│ ├── index.astro # Landing page
9186│ ├── explore.astro # Browse public collections
92- │ ├── connect.astro # Integration guide (for devs and LLMs)
93- │ ├── login.astro # Login form
94- │ ├── signup.astro # Signup form
95- │ ├── dashboard.astro # Authenticated user's collections
87+ │ ├── login/signup.astro
88+ │ ├── dashboard.astro # User's collections
9689│ ├── settings/ # Account settings + API key management
97- │ ├── blog/ # Blog posts
90+ │ ├── blog/ # Markdown blog posts
9891│ ├── docs/ # Documentation (concepts, quickstart, API ref, self-hosting)
9992│ └── [owner]/ # Dynamic routes
100- │ ├── index.astro # /:owner — account profile
93+ │ ├── index.astro # Profile page
94+ │ ├── settings.astro # Account/org settings
10195│ └── [collection]/
102- │ ├── index.astro # /:owner/:collection — collection overview
103- │ └── v/[n].astro # /:owner/:collection/v/:n — version detail
104- ├── styles/
105- │ └── global.css # Tailwind theme (parchment/ink palette)
96+ │ ├── index.astro # Collection overview
97+ │ ├── versions.astro # Version history
98+ │ ├── diff.astro # Version diff viewer
99+ │ ├── settings.astro # Collection settings
100+ │ └── v/[n].astro # Version detail
101+ ├── styles/global.css # Tailwind theme (parchment/ink palette)
102+ public/
103+ ├── .well-known/ai.txt # Machine-readable API docs (for LLMs/bots)
106104tools/
107- ├── backupDb.ts # Postgres backup → S3
108- └── cron.ts # Scheduled task runner
105+ ├── backupDb.ts # Postgres backup → S3 (_backups/ prefix)
106+ └── cron.ts # Scheduled task runner (daily backups)
109107```
110108
111- ## API
112-
113- All endpoints are under ` /api ` . Auth via ` Authorization: Bearer <api_key> ` for writes.
114-
115- ### Accounts
116- | Method | Endpoint | Description |
117- | --------| ----------| -------------|
118- | POST | ` /api/accounts/signup ` | Create account |
119- | POST | ` /api/accounts/login ` | Log in (sets session cookie) |
120- | POST | ` /api/accounts/logout ` | Log out |
121- | GET | ` /api/accounts/me ` | Current user |
122- | GET | ` /api/accounts/:slug ` | Public profile |
123- | POST | ` /api/accounts/keys ` | Create API key |
124- | GET | ` /api/accounts/keys ` | List API keys |
125- | DELETE | ` /api/accounts/keys/:id ` | Revoke API key |
126-
127- ### Collections
128- | Method | Endpoint | Description |
129- | --------| ----------| -------------|
130- | GET | ` /api/collections ` | Browse public collections |
131- | POST | ` /api/accounts/:owner/collections ` | Create collection |
132- | GET | ` /api/collections/:owner/:slug ` | Collection metadata |
133- | PATCH | ` /api/collections/:owner/:slug ` | Update collection |
134- | DELETE | ` /api/collections/:owner/:slug ` | Delete collection |
135- | GET | ` /api/accounts/:owner/collections ` | List owner's collections |
136-
137- ### Versions
138- | Method | Endpoint | Description |
139- | --------| ----------| -------------|
140- | POST | ` /api/collections/:owner/:slug/versions ` | Push a version |
141- | GET | ` /api/collections/:owner/:slug/versions ` | List versions |
142- | GET | ` /api/collections/:owner/:slug/versions/latest ` | Latest version |
143- | GET | ` /api/collections/:owner/:slug/versions/:n ` | Get version |
144- | GET | ` /api/collections/:owner/:slug/versions/:n/records ` | Get records |
145- | GET | ` /api/collections/:owner/:slug/versions/:n/manifest ` | Get manifest |
146- | GET | ` /api/collections/:owner/:slug/versions/:n/diff ` | Diff versions |
147-
148- ### Files
149- | Method | Endpoint | Description |
150- | --------| ----------| -------------|
151- | HEAD | ` /api/collections/:owner/:slug/files/:hash ` | Check existence |
152- | GET | ` /api/collections/:owner/:slug/files/:hash ` | Download |
153- | PUT | ` /api/collections/:owner/:slug/files/:hash ` | Upload |
109+ ## Deployment
154110
155- ## Scripts
111+ ### Infrastructure
156112
157- ``` bash
158- npm run dev # Start full dev environment (Docker)
159- npm run dev:server # Start Astro + API (no Docker)
160- npm run build # Build for production
161- npm run start # Start production server
113+ - ** Hetzner** — Single box (8 vCPU, 16GB RAM) running Docker Swarm
114+ - ** Caddy** — Host-level reverse proxy, TLS via ` tls internal ` (Cloudflare Full mode)
115+ - ** Cloudflare** — DNS + CDN + DDoS protection
116+ - ** R2** — Object storage (zero egress fees), single bucket with prefixes:
117+ - ` files/ ` — Content-addressed immutable uploads
118+ - ` _backups/ ` — Compressed Postgres dumps
162119
163- npm run db:generate # Generate Drizzle migrations
164- npm run db:migrate # Run migrations
165- npm run db:seed # Seed database (--force to re-seed)
120+ ### Stacks
166121
167- npm run tool:backup # Manual database backup to S3
122+ Two Docker Swarm stacks run on the same box:
168123
169- npm run secrets:encrypt # Encrypt .env with SOPS
170- npm run secrets:decrypt # Decrypt .env.enc with SOPS
171- ```
124+ | Stack | Domain | Host Ports | Purpose |
125+ | -------| --------| -----------| ---------|
126+ | ` underlay-prod ` | www.underlay.org | 4322 (SSR), 3001 (API) | Production |
127+ | ` underlay-dev ` | dev.underlay.org | 4321 (SSR), 3000 (API) | Staging |
172128
173- ## Deployment
129+ Container-internal ports are always fixed: 4321 (Astro) and 3000 (Fastify).
130+ Host ports are configured via ` APP_PORT ` and ` API_PORT ` in .env files (compose-only variables).
174131
175- ### Docker
132+ ### CI/CD Flow
176133
177- ``` bash
178- docker build -t underlay .
179- docker compose up -d
180- ```
134+ 1 . Push to ` main ` → deploys to ` dev.underlay.org `
135+ 2 . Create a release/tag → deploys to ` www.underlay.org `
136+ 3 . Manual dispatch → choose environment
181137
182- The production ` docker-compose.yml ` runs three services:
183- - ** postgres** — PostgreSQL 16 with persistent volume
184- - ** app** — Migrations + Astro SSR + Fastify API
185- - ** cron** — Scheduled database backups
138+ The workflow: build Docker image → push to GHCR → SSH to server → decrypt secrets → ` docker stack deploy ` → wait for healthy rollout.
186139
187- ### CI/CD
140+ Required GitHub secrets: ` SSH_PRIVATE_KEY ` , ` SSH_HOST_DEV ` , ` SSH_HOST_PROD ` , ` SSH_USER ` , ` GHCR_USER ` , ` GHCR_TOKEN ` .
188141
189- Push to ` main ` triggers the GitHub Actions workflow:
190- 1 . Build Docker image → push to GHCR
191- 2 . SSH to server → pull → decrypt secrets → ` docker compose up `
142+ ### Docker Compose Files
192143
193- Required GitHub secrets: ` SSH_PRIVATE_KEY ` , ` SSH_HOST ` , ` SSH_USER ` , ` GHCR_USER ` , ` GHCR_TOKEN ` .
144+ | File | Purpose |
145+ | ------| ---------|
146+ | ` docker-compose.yml ` | Deployed stacks (prod & dev via Swarm) |
147+ | ` docker-compose.local.yml ` | Local development (source-mounted, MinIO, hot reload) |
194148
195149## Environment Variables
196150
@@ -201,11 +155,60 @@ Required GitHub secrets: `SSH_PRIVATE_KEY`, `SSH_HOST`, `SSH_USER`, `GHCR_USER`,
201155| ` APP_PORT ` | Host-published port for Astro SSR (compose only, default: 4322) |
202156| ` API_PORT ` | Host-published port for Fastify API (compose only, default: 3001) |
203157| ` S3_BUCKET ` | S3 bucket name |
204- | ` S3_REGION ` | S3 region |
205- | ` S3_ENDPOINT ` | S3 endpoint (for MinIO, R2, etc.) |
158+ | ` S3_REGION ` | S3 region ( ` auto ` for R2) |
159+ | ` S3_ENDPOINT ` | S3 endpoint URL |
206160| ` S3_ACCESS_KEY ` | S3 access key |
207161| ` S3_SECRET_KEY ` | S3 secret key |
208162
163+ ` NODE_ENV ` is set in ` docker-compose.yml ` ` environment: ` block (not in .env files).
164+
165+ ## Scripts
166+
167+ ``` bash
168+ # Development
169+ npm run dev # Start full local stack (Docker)
170+ npm run dev:server # Start Astro + API without Docker
171+ npm run build # Build for production
172+
173+ # Database
174+ npm run db:generate # Generate Drizzle migrations from schema changes
175+ npm run db:migrate # Run pending migrations
176+ npm run db:seed # Seed database
177+
178+ # Tools
179+ npm run tool:backup # Manual database backup to S3
180+
181+ # Secrets (SOPS + age)
182+ npm run secrets:encrypt # Encrypt .env → .env.enc
183+ npm run secrets:encrypt:dev # Encrypt .env.dev → .env.dev.enc
184+ npm run secrets:decrypt # Decrypt .env.enc → .env
185+ npm run secrets:decrypt:dev # Decrypt .env.dev.enc → .env.dev
186+ ```
187+
188+ ## Maintenance Checklist
189+
190+ When adding or changing features, update these locations:
191+
192+ | What | Where | Purpose |
193+ | ------| -------| ---------|
194+ | API documentation | ` public/.well-known/ai.txt ` | Machine-readable docs for LLMs and bots |
195+ | Concepts | ` src/pages/docs/concepts.astro ` | Core concepts explanation |
196+ | API reference | ` src/pages/docs/api/*.astro ` | Endpoint-level docs with examples |
197+ | Integration guide | ` src/pages/docs/integration.astro ` | Developer onboarding guide |
198+ | Quick start | ` src/pages/docs/quickstart.astro ` | Getting started tutorial |
199+ | Self-hosting | ` src/pages/docs/self-host.astro ` | Deployment instructions |
200+ | DB schema | ` src/db/schema.ts ` → ` npm run db:generate ` | Schema changes need a migration |
201+ | Encrypted secrets | ` .env.enc ` / ` .env.dev.enc ` | Re-encrypt after changing .env files |
202+
203+ ### Privacy features
204+
205+ The system supports three levels of privacy (type-level, field-level, record-level) via ` "private": true ` annotations. When changing how privacy works, update:
206+ - ` src/api/routes/versions.ts ` — filtering logic
207+ - ` src/api/routes/files.ts ` — file access checks
208+ - ` public/.well-known/ai.txt ` — Privacy section
209+ - ` src/pages/docs/concepts.astro ` — Privacy section
210+ - ` src/pages/docs/api/versions.astro ` — Push endpoint docs
211+
209212## License
210213
211214MIT
0 commit comments