remove preact and create setup wizard
This commit is contained in:
@@ -11,9 +11,9 @@
|
||||
|
||||
| Document | Decision |
|
||||
|----------|----------|
|
||||
| [ADR-001: Tech Stack](adrs/adr-001-tech-stack.md) | Go backend + Preact frontend |
|
||||
| [ADR-001: Tech Stack](adrs/adr-001-tech-stack.md) | Go server with embedded web assets |
|
||||
| [ADR-002: Sync Protocol](adrs/adr-002-sync-protocol.md) | SimpleSync content-addressed protocol |
|
||||
| [ADR-003: SSR Strategy](adrs/adr-003-ssr-strategy.md) | Go templates with Preact hydration |
|
||||
| [ADR-003: SSR Strategy](adrs/adr-003-ssr-strategy.md) | Go templates with embedded browser scripts |
|
||||
| [ADR-004: Auth Strategy](adrs/adr-004-auth-strategy.md) | Passkeys primary, passwords fallback |
|
||||
| [ADR-005: Storage Strategy](adrs/adr-005-storage-strategy.md) | libsql + content-addressed filesystem |
|
||||
| [ADR-006: Email Strategy](adrs/adr-006-email-strategy.md) | Postmark gateway, plain-text only |
|
||||
@@ -84,8 +84,7 @@ cairnquire/
|
||||
│ ├── security-specification.md
|
||||
│ └── deployment-guide.md
|
||||
├── apps/
|
||||
│ ├── server/ # Go application
|
||||
│ └── web/ # Preact frontend
|
||||
│ └── server/ # Go application, templates, and static assets
|
||||
├── packages/
|
||||
│ └── protocol/ # Shared types
|
||||
├── docker-compose.yml
|
||||
|
||||
@@ -1,53 +1,54 @@
|
||||
# ADR-001: Go Backend with Preact Frontend
|
||||
# ADR-001: Go Server with Embedded Web Assets
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
We need to choose a backend language/framework and frontend technology for a documentation platform with these requirements:
|
||||
- Ultra-fast performance
|
||||
- Minimal external dependencies
|
||||
- No vendor lock-in
|
||||
- Battle-tested security
|
||||
- Server-side rendering for non-interactive pages
|
||||
- Offline-capable web client
|
||||
We need a documentation platform with fast page delivery, minimal external
|
||||
dependencies, secure server-side rendering, and a straightforward deployment
|
||||
artifact.
|
||||
|
||||
## Decision
|
||||
**Backend**: Go 1.22+ with standard library plus minimal, well-audited packages
|
||||
**Frontend**: Preact 10 with TypeScript, Vite build system
|
||||
Use Go 1.24+ with the standard library plus minimal, audited packages. The Go
|
||||
binary serves HTML templates and embeds its CSS, images, and small browser
|
||||
scripts. There is no separate SPA, Node dependency tree, or frontend build
|
||||
pipeline.
|
||||
|
||||
Browser scripts progressively add editing, offline caching, WebSocket updates,
|
||||
comments, and authentication interactions to server-rendered pages.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Go's standard library is comprehensive and security-audited
|
||||
- Single static binary deployment — no runtime dependencies
|
||||
- Preact is 10KB (vs 40KB+ React) with identical API
|
||||
- Go's `html/template` provides safe SSR with auto-escaping
|
||||
- TypeScript gives type safety without runtime overhead
|
||||
- Both ecosystems have excellent supply-chain security (Go modules with checksums, npm audit)
|
||||
- One application binary and one build toolchain
|
||||
- Go `html/template` provides auto-escaped server rendering
|
||||
- Embedded assets deploy with the server binary
|
||||
- Fewer supply-chain dependencies and fewer moving parts in CI
|
||||
- Pages remain readable before browser scripts execute
|
||||
|
||||
### Negative
|
||||
- Go template syntax is less expressive than JSX
|
||||
- WebSocket sync logic must be implemented manually (no framework like Socket.io)
|
||||
- Preact ecosystem smaller than React (mitigated by using mostly native APIs)
|
||||
- Template syntax is less expressive than JSX
|
||||
- Browser behavior uses small framework-free modules
|
||||
- WebSocket and offline behavior must be maintained directly
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Separate Preact SPA
|
||||
- **Pros**: Component model, client-side routing, TypeScript ecosystem
|
||||
- **Cons**: Adds Node tooling, package dependencies, a second build pipeline,
|
||||
and duplicate UI surfaces
|
||||
- **Rejected**: The Go-served UI already covers the application surface
|
||||
|
||||
### Rust + Axum
|
||||
- **Pros**: Memory safety guarantees, excellent performance
|
||||
- **Cons**: Longer compile times, smaller talent pool, steeper learning curve for team
|
||||
- **Rejected**: Go's pragmatic balance of safety and velocity better fits project timeline
|
||||
|
||||
### Deno Fresh
|
||||
- **Pros**: Island architecture, native TypeScript, no build step
|
||||
- **Cons**: Newer ecosystem, fewer audited libraries, Deno runtime less battle-tested than Go
|
||||
- **Rejected**: Go's maturity and deployment simplicity preferred
|
||||
- **Cons**: Longer compile times and steeper learning curve
|
||||
- **Rejected**: Go better fits the project deployment model
|
||||
|
||||
### Next.js / React Server Components
|
||||
- **Pros**: Mature ecosystem, built-in SSR
|
||||
- **Cons**: Heavy bundle size, complex build system, RSC lock-in, many dependencies
|
||||
- **Rejected**: Violates "minimal dependencies" and "no vendor lock-in" principles
|
||||
- **Cons**: Heavy build system and more runtime complexity
|
||||
- **Rejected**: Violates the minimal dependency goal
|
||||
|
||||
## References
|
||||
- Go Security Policy: https://go.dev/security
|
||||
- Preact Size Comparison: https://bundlephobia.com/package/preact@10.19.3
|
||||
- Go `html/template`: https://pkg.go.dev/html/template
|
||||
|
||||
@@ -1,67 +1,55 @@
|
||||
# ADR-003: SSR Strategy - Go Templates with Preact Hydration
|
||||
# ADR-003: SSR Strategy - Go Templates with Embedded Browser Scripts
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The application needs to serve document pages that are readable without JavaScript (for performance and accessibility), while providing rich interactivity (comments, editing, search) when JavaScript is available. We need a server-side rendering strategy that works with our Go backend and Preact frontend.
|
||||
Document pages must be readable quickly and safely while still supporting
|
||||
editing, authentication, comments, offline caching, and real-time updates.
|
||||
|
||||
## Decision
|
||||
**Strategy C**: Go's `html/template` renders initial HTML for all pages. Preact hydrates interactive components on the client.
|
||||
Go `html/template` renders complete HTML pages. The server embeds and serves
|
||||
small framework-free browser scripts for progressive enhancement.
|
||||
|
||||
### Implementation Details
|
||||
|
||||
1. **Server Rendering**:
|
||||
- Go handlers query database for document content
|
||||
- Markdown parsed to HTML server-side (Goldmark)
|
||||
- `html/template` renders full page with document HTML embedded
|
||||
- No JavaScript required for reading
|
||||
- Go handlers query document and account data
|
||||
- Goldmark parses Markdown to HTML server-side
|
||||
- `html/template` renders complete pages
|
||||
- Pages are readable before JavaScript executes
|
||||
|
||||
2. **Client Hydration**:
|
||||
- Preact components mount into designated DOM containers
|
||||
- Comments, search, editor are Preact islands
|
||||
- Static content remains server-rendered HTML
|
||||
- `preact-iso` handles client-side navigation between pages
|
||||
|
||||
3. **Data Passing**:
|
||||
- Initial state embedded as JSON in `<script type="application/json">` tags
|
||||
- Preact reads initial state during hydration, avoids extra API calls
|
||||
- CSP nonce applied to all inline scripts
|
||||
2. **Browser Enhancement**:
|
||||
- Embedded scripts handle editing, comments, authentication, offline cache,
|
||||
service-worker behavior, and WebSocket updates
|
||||
- Native browser APIs are preferred over a client framework
|
||||
- There is no separate SPA route or frontend build pipeline
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- First contentful paint is instant (pure HTML)
|
||||
- SEO-friendly without extra complexity
|
||||
- Graceful degradation: works without JS
|
||||
- Go templates are auto-escaping XSS-safe
|
||||
- No V8/JS runtime embedded in Go binary
|
||||
- Smaller client bundle (only interactive components)
|
||||
- Fast first contentful paint
|
||||
- Auto-escaped templates
|
||||
- One deployable application artifact
|
||||
- Small browser payloads and no Node build dependency
|
||||
- Interactive features stay close to their HTTP endpoints
|
||||
|
||||
### Negative
|
||||
- Some code duplication: markdown parsing logic in both Go and JS (for preview)
|
||||
- Template syntax is verbose compared to JSX
|
||||
- Hydration can cause flicker if not carefully managed
|
||||
- Client state must reconcile with server-rendered HTML
|
||||
- Browser modules need direct DOM event handling
|
||||
- Shared UI abstractions require deliberate maintenance
|
||||
- More complex browser interactions need focused integration tests
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Go Templates Only (No Preact)
|
||||
- **Pros**: Zero client JS, maximum performance
|
||||
- **Cons**: No interactive features (comments, real-time sync, search)
|
||||
- **Rejected**: Doesn't meet collaboration requirements
|
||||
### Separate Hydrated SPA
|
||||
- **Pros**: Component framework and client-side routing
|
||||
- **Cons**: Duplicates UI paths and introduces a second toolchain
|
||||
- **Rejected**: The server-rendered UI already supports the required behavior
|
||||
|
||||
### Embedded JS Runtime (QuickJS/V8)
|
||||
- **Pros**: True isomorphic rendering, JSX templates
|
||||
- **Cons**: Adds C dependency, complex build, larger binary, security surface area
|
||||
- **Rejected**: Violates minimal dependency principle
|
||||
|
||||
### Preact SSR via WASM
|
||||
- **Pros**: Same templates on server and client
|
||||
- **Cons**: WASM overhead, complex build pipeline, slower than Go templates
|
||||
- **Rejected**: Go templates are faster and simpler
|
||||
### Embedded JS Runtime
|
||||
- **Pros**: Isomorphic rendering
|
||||
- **Cons**: Larger binary and additional security surface
|
||||
- **Rejected**: Go templates are simpler
|
||||
|
||||
## References
|
||||
- Go html/template security: https://pkg.go.dev/html/template
|
||||
- Preact hydration: https://preactjs.com/guide/v10/server-side-rendering
|
||||
- Islands Architecture: https://jasonformat.com/islands-architecture/
|
||||
- Go `html/template`: https://pkg.go.dev/html/template
|
||||
|
||||
@@ -12,7 +12,7 @@ Cairnquire is a self-hosted documentation platform. A single Go binary serves HT
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
|
||||
│ │ Browser │ │ File Editor │ │ Mobile / PWA │ │
|
||||
│ │ (Preact SPA) │ │ (VS Code, │ │ (Responsive Web) │ │
|
||||
│ │ (Go HTML UI) │ │ (VS Code, │ │ (Responsive Web) │ │
|
||||
│ │ │ │ Vim, etc.) │ │ │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └────────────┬─────────────┘ │
|
||||
│ │ │ │ │
|
||||
@@ -142,7 +142,7 @@ Cairnquire is a self-hosted documentation platform. A single Go binary serves HT
|
||||
8. Render HTML: AST → html/template with layout
|
||||
│
|
||||
▼
|
||||
9. Response: HTML with CSP nonce, Preact hydration marker
|
||||
9. Response: HTML with embedded static assets and progressive enhancement scripts
|
||||
```
|
||||
|
||||
## Data Flow: File Sync
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
- [ ] Initialize monorepo structure
|
||||
- `apps/server/` with Go module
|
||||
- `apps/web/` with Vite + Preact + TypeScript
|
||||
- embedded templates and static assets under `apps/server/internal/httpserver/`
|
||||
- `packages/protocol/` with protobuf schema
|
||||
- `docs/` with existing documentation
|
||||
|
||||
@@ -53,10 +53,10 @@
|
||||
- Security headers middleware
|
||||
- MIME type detection
|
||||
|
||||
- [ ] Initial frontend
|
||||
- Preact app setup with Vite
|
||||
- Basic routing (preact-iso)
|
||||
- Hydration entry point
|
||||
- [ ] Initial browser UI
|
||||
- Go template routes
|
||||
- Embedded framework-free browser scripts
|
||||
- Progressive enhancement entry points
|
||||
- CSS design system foundation
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- Comment threading (parent/child)
|
||||
- Line/section anchoring (hash-based)
|
||||
- Comment resolution (mark as resolved)
|
||||
- Comment display in Preact UI
|
||||
- Comment display in the Go-served browser UI
|
||||
|
||||
- [ ] Notification system
|
||||
- Notification queue in database
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
- [ ] Unit tests
|
||||
- Go: >80% coverage for all packages
|
||||
- Preact: Component testing with uvu
|
||||
- Browser behavior: integration testing for embedded scripts
|
||||
- Mock external dependencies (email, filesystem)
|
||||
|
||||
- [ ] Integration tests
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
## 2026-04-28
|
||||
|
||||
1. The plan and ADRs repeatedly describe a single Go binary, while Milestone 1 also requires `apps/web/` as a separate Vite/Preact app. The current implementation treats the web app as build-time static assets served by the Go binary.
|
||||
1. Resolved: the separate Vite/Preact SPA was removed. The Go binary serves templates and embedded static assets directly.
|
||||
2. Milestone 1 asks for `packages/protocol/` with a protobuf schema, but the sync ADR and protocol spec describe JSON messages over WebSocket. The protocol package currently preserves both a `.proto` placeholder and TypeScript-facing JSON model notes.
|
||||
3. ADR-005 stores content under `data/files/...`, while Milestone 2 examples use `store/...`. The implementation follows ADR-005 and uses `data/files`.
|
||||
4. The frontend stack still requires a Node package install step, but local automation is now centralized in `mise` tasks and the lockfile-backed `pnpm` workflow rather than ad hoc commands.
|
||||
4. Resolved: the deployment and development toolchain no longer require Node or pnpm.
|
||||
5. The foundation milestone specifies `golang-migrate`, but `go-libsql` integration constraints need a clean validation path. A lightweight internal migrator is used now so the project can move forward.
|
||||
|
||||
@@ -29,7 +29,7 @@ A minimal-dependency, ultra-fast documentation platform that publishes markdown
|
||||
- [x] Markdown-to-HTML pipeline with most common extensions enabled
|
||||
- [x] Static file serving for attachments
|
||||
- [x] Basic Dockerfile and docker-compose.yml
|
||||
- [x] Dev mode with `air` live reload + Vite HMR proxy
|
||||
- [x] Dev mode with `air` live reload
|
||||
- [x] Miller column document browser with folder navigation
|
||||
- [x] `index.md` default rendering for folders
|
||||
- [x] Content-addressed attachment storage with secure serving
|
||||
@@ -47,7 +47,7 @@ A minimal-dependency, ultra-fast documentation platform that publishes markdown
|
||||
|
||||
**Notes:**
|
||||
- Mermaid diagrams and math blocks now render client-side after server-rendered markdown loads.
|
||||
- Dev mode uses `air` for Go live reload and Vite proxy for frontend HMR.
|
||||
- Dev mode uses `air` for Go live reload.
|
||||
|
||||
---
|
||||
|
||||
@@ -209,8 +209,8 @@ A minimal-dependency, ultra-fast documentation platform that publishes markdown
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Client Browser │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
||||
│ │ Preact UI │ │ File Sync │ │ Search Index │ │
|
||||
│ │ (hydrated) │ │ (IndexedDB) │ │ (FTS5) │ │
|
||||
│ │ Go HTML UI │ │ File Sync │ │ Search Index │ │
|
||||
│ │ + scripts │ │ (IndexedDB) │ │ (FTS5) │ │
|
||||
│ └──────────────┘ └──────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
@@ -259,17 +259,14 @@ See [architecture-overview.md](architecture-overview.md) for detailed component
|
||||
| Crypto | `crypto/ed25519`, `crypto/sha256` | Standard library, no external deps |
|
||||
| Templates | `html/template` | Stdlib, auto-escaping, SSR-safe |
|
||||
|
||||
### Frontend (Preact + TypeScript)
|
||||
### Browser UI
|
||||
|
||||
| Component | Library | Justification |
|
||||
| --------- | ------------------------------ | --------------------------------------- |
|
||||
| Framework | `preact` (10KB) | React API, tiny bundle, fast hydration |
|
||||
| Routing | `preact-iso` | Lightweight isomorphic routing |
|
||||
| Markdown | `marked` + `micromark` | Server+client compatible |
|
||||
| Search | `flexsearch` | Client-side full-text, zero server load |
|
||||
| Styling | Native CSS + custom properties | No build-time CSS processing needed |
|
||||
| Icons | Inline SVG | No icon font dependency |
|
||||
| Build | `vite` | Fast HMR, modern output, minimal config |
|
||||
| Component | Technology | Justification |
|
||||
| ----------- | ------------------------------ | -------------------------------------- |
|
||||
| Rendering | Go `html/template` | Server-rendered, auto-escaped HTML |
|
||||
| Interaction | Embedded browser scripts | Small progressive-enhancement modules |
|
||||
| Styling | Native CSS + custom properties | No build-time CSS processing needed |
|
||||
| Icons | Inline SVG | No icon font dependency |
|
||||
|
||||
### Infrastructure
|
||||
|
||||
@@ -347,12 +344,8 @@ cairnquire/
|
||||
│ │ │ ├── markdown/ # Markdown rendering pipeline
|
||||
│ │ │ ├── realtime/ # WebSocket hub
|
||||
│ │ │ └── store/ # Content-addressed filesystem
|
||||
│ │ ├── static/ # Static assets
|
||||
│ │ ├── static/ # Embedded static assets
|
||||
│ │ └── templates/ # Go HTML templates
|
||||
│ └── web/ # Preact frontend application
|
||||
│ ├── src/
|
||||
│ ├── index.html
|
||||
│ └── vite.config.ts
|
||||
├── content/ # Markdown source files
|
||||
├── data/ # SQLite database & attachments
|
||||
└── packages/
|
||||
|
||||
@@ -8,6 +8,8 @@ Browser users authenticate with passkeys or password fallback. Successful login
|
||||
|
||||
Endpoints:
|
||||
|
||||
- `GET /setup`
|
||||
- `POST /api/setup`
|
||||
- `GET /login`
|
||||
- `GET /account`
|
||||
- `POST /api/auth/register/password`
|
||||
@@ -21,7 +23,12 @@ Endpoints:
|
||||
- `POST /api/auth/passkeys/login/begin`
|
||||
- `POST /api/auth/passkeys/login/finish?challengeId=...`
|
||||
|
||||
The first registered user bootstraps as `admin`. Later public registrations become `viewer`; role changes are admin-managed from the account screen. Public registration endpoints only create new accounts. Adding or changing credentials on an existing account requires an authenticated browser session.
|
||||
On an empty database, the one-time `/setup` wizard creates the first `admin`
|
||||
account and records whether public signup is enabled. Later public
|
||||
registrations are accepted only when that setting is enabled and always become
|
||||
`viewer`; role changes are admin-managed from the account screen. Public
|
||||
registration endpoints only create new accounts. Adding or changing
|
||||
credentials on an existing account requires an authenticated browser session.
|
||||
|
||||
## API Tokens
|
||||
|
||||
@@ -64,7 +71,8 @@ This is intentionally inspired by OAuth device authorization, but it is not a ge
|
||||
|
||||
## Route Protection
|
||||
|
||||
- Public: health, static assets, document reads, login/account entry pages, password/passkey begin-login/register endpoints, device start/token polling.
|
||||
- Setup-only until configured: initial setup page and endpoint.
|
||||
- Public: health, static assets, document reads, login/account entry pages, password/passkey begin-login/register endpoints when signups are enabled, device start/token polling.
|
||||
- Session or bearer token required: auth profile/logout, edit/save APIs, uploads, sync/content APIs, device approval.
|
||||
- Admin role required: admin APIs and API token management.
|
||||
|
||||
|
||||
@@ -4,269 +4,120 @@
|
||||
|
||||
- Docker 24.0+
|
||||
- Docker Compose 2.20+
|
||||
- 1GB RAM minimum (2GB recommended)
|
||||
- 10GB disk space
|
||||
- TLS certificate (Let's Encrypt or custom)
|
||||
- A reverse proxy that terminates TLS for the public hostname
|
||||
- A writable checkout directory for `./data` and `./content`
|
||||
|
||||
## Quick Start
|
||||
## Fresh Deployment
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/your-org/cairnquire.git
|
||||
cd cairnquire
|
||||
|
||||
# Copy and edit configuration
|
||||
cp .env.example .env
|
||||
# Edit .env with your domain, email, Postmark API key
|
||||
|
||||
# Start services
|
||||
docker-compose up -d
|
||||
|
||||
# Verify health
|
||||
curl https://your-domain.com/health
|
||||
```
|
||||
|
||||
## Configuration
|
||||
Edit `.env` and set the HTTPS origin that users will open in their browser:
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `DOMAIN` | Yes | — | Public domain name |
|
||||
| `DB_PATH` | No | `/data/db.sqlite` | Database file path |
|
||||
| `FILESTORE_PATH` | No | `/data/files` | Content-addressed file storage |
|
||||
| `NOTES_PATH` | No | `/notes` | Watched markdown directory |
|
||||
| `POSTMARK_API_KEY` | Yes | — | Postmark server API token |
|
||||
| `POSTMARK_FROM` | No | `notifications@DOMAIN` | From email address |
|
||||
| `SESSION_SECRET` | Yes | — | 32-byte hex for session signing |
|
||||
| `ENV` | No | `production` | `development` or `production` |
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./notes:/notes:ro
|
||||
environment:
|
||||
- DOMAIN=${DOMAIN}
|
||||
- DB_PATH=/data/db.sqlite
|
||||
- FILESTORE_PATH=/data/files
|
||||
- NOTES_PATH=/notes
|
||||
- POSTMARK_API_KEY=${POSTMARK_API_KEY}
|
||||
- SESSION_SECRET=${SESSION_SECRET}
|
||||
env_file:
|
||||
- .env
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:noexec,nosuid,size=100m
|
||||
|
||||
# Optional: Traefik for auto-HTTPS
|
||||
traefik:
|
||||
image: traefik:v3.0
|
||||
command:
|
||||
- "--api.insecure=false"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
|
||||
- "--certificatesresolvers.letsencrypt.acme.email=${EMAIL}"
|
||||
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./letsencrypt:/letsencrypt
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
restart: unless-stopped
|
||||
```dotenv
|
||||
CAIRNQUIRE_PUBLIC_ORIGIN=https://cairnquire.example.com
|
||||
```
|
||||
|
||||
## Production Checklist
|
||||
|
||||
### Pre-Deployment
|
||||
- [ ] Change default `SESSION_SECRET` (generate: `openssl rand -hex 32`)
|
||||
- [ ] Configure `POSTMARK_API_KEY` with valid token
|
||||
- [ ] Set `DOMAIN` to public hostname
|
||||
- [ ] Configure DNS A/AAAA records
|
||||
- [ ] Set up SPF and DKIM for email domain
|
||||
- [ ] Review `.env` file — no default secrets
|
||||
|
||||
### Security
|
||||
- [ ] TLS 1.3 enforced (Traefik or reverse proxy)
|
||||
- [ ] HSTS header configured
|
||||
- [ ] Firewall rules: only 80/443 open
|
||||
- [ ] Fail2ban or rate limiting on SSH
|
||||
- [ ] Automatic security updates enabled
|
||||
- [ ] Backup encryption key stored securely
|
||||
|
||||
### Monitoring
|
||||
- [ ] Health check endpoint accessible
|
||||
- [ ] Log rotation configured
|
||||
- [ ] Disk space alerting (>80%)
|
||||
- [ ] Memory usage alerting
|
||||
- [ ] Uptime monitoring (external)
|
||||
|
||||
## Backup Procedures
|
||||
|
||||
### Automated Backup
|
||||
Start the application and verify the local container health endpoint:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup.sh
|
||||
|
||||
BACKUP_DIR="/backups"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_FILE="$BACKUP_DIR/md-hub-$TIMESTAMP.tar.gz"
|
||||
|
||||
# Create backup
|
||||
tar czf "$BACKUP_FILE" \
|
||||
-C /data \
|
||||
db.sqlite \
|
||||
files/
|
||||
|
||||
# Encrypt (optional)
|
||||
gpg --symmetric --cipher-algo AES256 "$BACKUP_FILE"
|
||||
rm "$BACKUP_FILE"
|
||||
|
||||
# Keep only last 30 days
|
||||
find "$BACKUP_DIR" -name "md-hub-*.tar.gz.gpg" -mtime +30 -delete
|
||||
docker compose up -d --build
|
||||
docker compose ps
|
||||
curl http://127.0.0.1:8080/health
|
||||
```
|
||||
|
||||
**Cron**: `0 2 * * * /opt/cairnquire/backup.sh`
|
||||
Then open `https://cairnquire.example.com/setup`. On an empty database the
|
||||
first-run wizard:
|
||||
|
||||
### Restore from Backup
|
||||
1. Creates the initial administrator account.
|
||||
2. Sets its password.
|
||||
3. Records whether visitors may create their own accounts.
|
||||
4. Signs the administrator in.
|
||||
|
||||
The setup endpoint is only available until the first administrator has been
|
||||
created. The signup setting can be changed later from the administrator's
|
||||
account page.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The checked-in Compose file sets the container paths. For a normal single-host
|
||||
deployment, only `CAIRNQUIRE_PUBLIC_ORIGIN` must be supplied in `.env`.
|
||||
|
||||
| Variable | Required | Compose default | Description |
|
||||
|----------|----------|-----------------|-------------|
|
||||
| `CAIRNQUIRE_PUBLIC_ORIGIN` | Yes | None | Exact external HTTPS origin. Passkeys and device-flow URLs depend on this value. |
|
||||
| `CAIRNQUIRE_EMAIL_SMTP_HOST` | No | `mailpit` | SMTP server used for notifications. |
|
||||
| `CAIRNQUIRE_EMAIL_SMTP_PORT` | No | `1025` | SMTP server port. |
|
||||
| `CAIRNQUIRE_EMAIL_FROM` | No | `notifications@cairnquire.local` | Notification sender address. |
|
||||
| `CAIRNQUIRE_LOG_LEVEL` | No | `INFO` | Application log level. |
|
||||
|
||||
The server also accepts these lower-level overrides when it is run outside the
|
||||
checked-in Compose deployment:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CAIRNQUIRE_SERVER_ADDR` | `:8080` | Listen address. |
|
||||
| `CAIRNQUIRE_DATABASE_PATH` | `../../data/db.sqlite` | Local SQLite/libsql database path. |
|
||||
| `CAIRNQUIRE_DATABASE_PRIMARY_URL` | Empty | Optional remote libsql primary URL for embedded-replica mode. |
|
||||
| `CAIRNQUIRE_DATABASE_AUTH_TOKEN` | Empty | Optional auth token for the remote libsql primary. |
|
||||
| `CAIRNQUIRE_CONTENT_SOURCE_DIR` | `../../content` | Writable Markdown source directory. |
|
||||
| `CAIRNQUIRE_CONTENT_STORE_DIR` | `../../data/files` | Content-addressed attachment store. |
|
||||
| `CAIRNQUIRE_CONFIG` | Empty | Optional JSON configuration file. Environment variables override it. |
|
||||
| `CAIRNQUIRE_DEV_MODE` | `false` | Local-only auth shortcut. Never enable this on a deployed server. |
|
||||
|
||||
## Persistent Data
|
||||
|
||||
Compose mounts:
|
||||
|
||||
- `./data:/workspace/data` for the database and attachments
|
||||
- `./content:/workspace/content` for editable Markdown source
|
||||
|
||||
Both mounts must remain writable because uploads, browser edits, and sync
|
||||
writebacks modify them.
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
Expose container port `8080` through a TLS-terminating reverse proxy. Preserve
|
||||
the original hostname and forwarded client address headers. The configured
|
||||
`CAIRNQUIRE_PUBLIC_ORIGIN` must exactly match the external origin, including
|
||||
`https://`.
|
||||
|
||||
## Backup And Restore
|
||||
|
||||
Stop writes or stop the app container before copying a fully consistent local
|
||||
database backup:
|
||||
|
||||
```bash
|
||||
# Stop application
|
||||
docker-compose stop app
|
||||
|
||||
# Extract backup
|
||||
tar xzf md-hub-20240115_020000.tar.gz -C /data
|
||||
|
||||
# Restart
|
||||
docker-compose up -d app
|
||||
|
||||
# Verify
|
||||
curl https://your-domain.com/health
|
||||
docker compose stop app
|
||||
tar czf cairnquire-backup.tgz data content
|
||||
docker compose up -d app
|
||||
```
|
||||
|
||||
## Upgrade Procedures
|
||||
Restore by stopping the app, replacing `data` and `content` from the archive,
|
||||
and starting the app again.
|
||||
|
||||
## Upgrade
|
||||
|
||||
### Minor Update (patch version)
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker compose stop app
|
||||
tar czf cairnquire-backup-before-upgrade.tgz data content
|
||||
git pull
|
||||
docker compose up -d --build
|
||||
curl http://127.0.0.1:8080/health
|
||||
```
|
||||
|
||||
### Major Update
|
||||
1. Read release notes for breaking changes
|
||||
2. Create backup: `./backup.sh`
|
||||
3. Pull new image: `docker-compose pull`
|
||||
4. Run database migrations (auto on startup)
|
||||
5. Update configuration if needed
|
||||
6. Deploy: `docker-compose up -d`
|
||||
7. Verify health and functionality
|
||||
8. Rollback if issues: restore from backup
|
||||
Database migrations run automatically on startup.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Application Won't Start
|
||||
```bash
|
||||
# Check logs
|
||||
docker-compose logs -f app
|
||||
|
||||
# Verify configuration
|
||||
docker-compose exec app /server -check-config
|
||||
|
||||
# Test database
|
||||
docker-compose exec app sqlite3 /data/db.sqlite ".tables"
|
||||
docker compose logs -f app
|
||||
docker compose ps
|
||||
curl http://127.0.0.1:8080/health
|
||||
```
|
||||
|
||||
### Sync Not Working
|
||||
```bash
|
||||
# Check file watcher
|
||||
docker-compose exec app ls -la /notes
|
||||
|
||||
# Verify WebSocket
|
||||
curl -i -N \
|
||||
-H "Connection: Upgrade" \
|
||||
-H "Upgrade: websocket" \
|
||||
-H "Sec-WebSocket-Key: $(openssl rand -base64 16)" \
|
||||
-H "Sec-WebSocket-Version: 13" \
|
||||
https://your-domain.com/ws
|
||||
```
|
||||
|
||||
### Email Not Sending
|
||||
```bash
|
||||
# Test Postmark connection
|
||||
curl -X POST \
|
||||
https://api.postmarkapp.com/server \
|
||||
-H "X-Postmark-Server-Token: YOUR_TOKEN" \
|
||||
-H "Accept: application/json"
|
||||
|
||||
# Check email queue
|
||||
docker-compose exec app sqlite3 /data/db.sqlite \
|
||||
"SELECT * FROM email_queue WHERE status='failed'"
|
||||
```
|
||||
|
||||
## Scaling
|
||||
|
||||
### Vertical Scaling
|
||||
- Increase container memory/CPU limits
|
||||
- SQLite WAL mode handles concurrent reads well
|
||||
- For heavy write loads, consider libsql replication
|
||||
|
||||
### Horizontal Scaling (Advanced)
|
||||
```yaml
|
||||
# Requires shared storage and load balancer
|
||||
services:
|
||||
app1:
|
||||
# ... same config ...
|
||||
environment:
|
||||
- DB_PATH=/shared/db.sqlite
|
||||
- FILESTORE_PATH=/shared/files
|
||||
|
||||
app2:
|
||||
# ... same config ...
|
||||
environment:
|
||||
- DB_PATH=/shared/db.sqlite
|
||||
- FILESTORE_PATH=/shared/files
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
ports:
|
||||
- "80:80"
|
||||
```
|
||||
|
||||
**Note**: Horizontal scaling requires:
|
||||
- Shared filesystem (NFS, EFS)
|
||||
- libsql replication or external database
|
||||
- Sticky sessions for WebSocket
|
||||
|
||||
## Support
|
||||
|
||||
- **Documentation**: `/docs` route (when authenticated)
|
||||
- **Health Check**: `GET /health`
|
||||
- **Metrics**: `GET /metrics` (Prometheus format)
|
||||
- **Logs**: `docker-compose logs -f app`
|
||||
- **Issues**: https://github.com/your-org/cairnquire/issues
|
||||
If passkey creation fails after deployment, confirm that the browser URL and
|
||||
`CAIRNQUIRE_PUBLIC_ORIGIN` are the same HTTPS origin.
|
||||
|
||||
@@ -408,7 +408,6 @@ RUN addgroup -g 1000 appgroup && \
|
||||
|
||||
# Read-only root filesystem
|
||||
COPY --from=builder /app/server /server
|
||||
COPY --from=builder /app/web/dist /web
|
||||
|
||||
# Data directory (writable)
|
||||
RUN mkdir /data && chown appuser:appgroup /data
|
||||
|
||||
@@ -37,7 +37,7 @@ Extend the existing web application so it works offline, supports concurrent edi
|
||||
|
||||
### Current State (Already Built)
|
||||
- Server: Go HTTP + SQLite + file watcher + WebSocket hub
|
||||
- Browser: Preact + IndexedDB cache + `navigator.onLine` detection
|
||||
- Browser: Go-served HTML + embedded scripts + IndexedDB cache + `navigator.onLine` detection
|
||||
- Documents served at `/:path*` with `index.md` default
|
||||
- Real-time updates pushed via WebSocket when files change on disk
|
||||
|
||||
|
||||
Reference in New Issue
Block a user