# weimar Architecture > A single-binary media repository for home servers — memes, group interest images, and videos with simple tag-based discovery. > Inspired by Chevereto V4 (`docs/CHEVERE.md`), scaled down for a small group on a single home server. --- ## 1. Design Principles 1. **Single binary** — Go backend with SvelteKit SPA (adapter-static) embedded via `embed.FS`. No Docker, no Node process, no external runtime. 2. **Zero external dependencies** at runtime (except optional `ffmpeg` for video thumbnails). 3. **Simple trumps scalable** — SQLite, filesystem storage, no queues, no caches. 4. **Authentication-first** — users are real accounts, not anonymous. 5. **CLI admin** — no web dashboard; admin operations are CLI commands. --- ## 2. Tech Stack | Layer | Technology | |-------|------------| | **Backend** | Go 1.22+ (net/http, http.ServeMux with method+pattern routing) | | **Frontend** | SvelteKit (adapter-static, pre-rendered SPA) | | **Database** | SQLite (via modernc.org/sqlite or mattn/go-sqlite3) | | **Session store** | SQLite (same database) | | **Image processing** | Go native (image/jpeg, image/png, image/gif, golang.org/x/image) | | **Avatar processing** | Go native (golang.org/x/image/draw), resize to 128px JPEG | | **Video thumbnails** | `ffmpeg` binary — optional, skipped if missing | | **EXIF stripping** | Go native (exif library) | --- ## 3. Directory Structure ``` weimar/ ├── go.mod / go.sum ├── weimar.toml # User-provided config file ├── AGENTS.md # Agent guide (commands, patterns, gotchas) │ ├── cmd/ │ └── weimar/ │ ├── main.go # Entry point: os.Exit on Execute() error │ ├── root.go # Persistent flags, viper config init, defaults │ ├── serve.go # weimar serve (HTTP server + //go:embed web/build/*) │ ├── users.go # weimar users (list/create/delete/password-reset) │ ├── image.go # weimar image delete │ └── version.go # weimar version │ ├── internal/ │ ├── server/ │ │ ├── server.go # HTTP server, route registration, embedded SPA │ │ └── middleware.go # requireAuth wrapper, writeJSONError │ ├── auth/ │ │ ├── session.go # Server-side session (cookie-based, SQLite-backed) │ │ └── register.go # Registration logic + sentinel errors │ ├── api/ │ │ ├── api.go # API struct (dependency holder), writeJSON/writeError │ │ ├── auth.go # POST /api/auth/login, /register, /logout │ │ ├── upload.go # POST /api/upload (file + tags + processing) │ │ ├── images.go # GET /api/images, GET /api/images/{id} │ │ ├── download.go # GET /api/images/{id}/download │ │ ├── thumbnail.go # GET /api/images/{id}/thumbnail │ │ ├── tags.go # GET /api/tags/suggest │ │ ├── users.go # GET /api/users/me │ │ └── avatar.go # POST /api/users/me/avatar, GET /api/users/{id}/avatar │ ├── db/ │ │ ├── sqlite.go # Database connection and migrations │ │ ├── models.go # Structs: User, Image, Session, Tag │ │ └── queries.go # All SQL CRUD operations │ ├── storage/ │ │ ├── disk.go # File read/write/delete, hash-partitioned paths │ │ └── processing.go # MIME detection, EXIF stripping, thumbnail generation │ └── config/ │ └── config.go # FromViper() reads typed Config from viper │ ├── web/ # SvelteKit SPA (adapter-static, CSR-only) │ ├── src/ │ │ ├── routes/ │ │ │ ├── +layout.svelte # Nav bar, auth check (getMe), logout │ │ │ ├── +layout.js # ssr=false, prerender=false │ │ │ ├── +page.svelte # Home page (constructivist design) │ │ │ ├── browse/+page.svelte # Gallery grid (6-col, tags, pagination, viewer) │ │ │ ├── login/+page.svelte # Login/Register form │ │ │ ├── upload/+page.svelte # File upload + tag autocomplete │ │ │ ├── settings/+page.svelte # Avatar upload, user info │ │ │ └── image/[id]/+page.svelte # Image detail + download │ │ └── lib/ │ │ ├── api.ts # Fetch wrapper, typed API functions │ │ ├── Avatar.svelte # Avatar component (image or SVG fallback) │ │ └── ImageViewer.svelte # Full-screen overlay viewer (keyboard nav, idle fade) │ ├── svelte.config.js # adapter-static │ └── package.json │ ├── data/ │ ├── images/ # Hash-partitioned image storage │ │ └── ab/cd/ef/. │ └── weimar.db # SQLite database │ └── docs/ ├── CHEVERE.md # Chevereto V4 reference architecture └── ARCHITECTURE.md # This file ``` --- ## 4. Data Model ### SQLite Tables **users** | Column | Type | Notes | |--------|------|-------| | id | INTEGER PRIMARY KEY | | | username | TEXT UNIQUE NOT NULL | | | password_hash | TEXT NOT NULL | bcrypt | | is_admin | INTEGER DEFAULT 0 | | | avatar_path | TEXT | Relative path in storage dir, nullable | | created_at | TEXT (ISO 8601) | | | updated_at | TEXT (ISO 8601) | | **sessions** | Column | Type | Notes | |--------|------|-------| | id | INTEGER PRIMARY KEY | | | user_id | INTEGER REFERENCES users(id) | | | token | TEXT UNIQUE NOT NULL | Random 64-char hex | | expires_at | TEXT (ISO 8601) | | | created_at | TEXT (ISO 8601) | | **images** | Column | Type | Notes | |--------|------|-------| | id | INTEGER PRIMARY KEY | | | filename | TEXT NOT NULL | Original filename | | storage_path | TEXT NOT NULL | Relative path in storage dir | | thumbnail_path | TEXT | Relative path, nullable (no ffmpeg) | | uploaded_by | INTEGER REFERENCES users(id) | | | file_size | INTEGER | Bytes | | width | INTEGER | | | height | INTEGER | | | mime_type | TEXT | e.g. image/jpeg, video/mp4 | | created_at | TEXT (ISO 8601) | | | updated_at | TEXT (ISO 8601) | | **image_tags** | Column | Type | Notes | |--------|------|-------| | image_id | INTEGER REFERENCES images(id) | ON DELETE CASCADE | | tag | TEXT NOT NULL | Lowercase, trimmed | **tags** (materialized for autocomplete) | Column | Type | Notes | |--------|------|-------| | tag | TEXT PRIMARY KEY | Lowercase, unique | | usage_count | INTEGER DEFAULT 0 | Denormalized for sort | --- ## 5. API Endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/api/auth/login` | No | Login, returns session cookie | | POST | `/api/auth/register` | No* | Create account (*if registration open) | | POST | `/api/auth/logout` | Yes | Clear session | | POST | `/api/upload` | Yes | Upload image/video + tags | | GET | `/api/images` | No* | Paginated listing, ?tags filter, newest-first *public unless require_auth_for_browse=true | | GET | `/api/images/{id}` | No* | Single image metadata (incl. tags_detail, uploader) | | GET | `/api/images/{id}/download` | No* | Serve original file | | GET | `/api/images/{id}/thumbnail` | No* | Serve thumbnail (JPEG, 300px max) | | GET | `/api/tags/suggest?q=` | Yes | Tag autocomplete (LIKE query by usage) | | POST | `/api/images/{id}/tags` | Yes | Add tags (owner only) | | DELETE | `/api/images/{id}/tags` | Yes | Remove tags (owner only) | | GET | `/api/users/me` | Yes | Current user info (username, avatar_url, is_admin) | | POST | `/api/users/me/avatar` | Yes | Upload avatar (resized to 128px JPEG) | | GET | `/api/users/{id}/avatar` | No | Serve avatar (Cache-Control: public, 86400s) | Browse endpoints (GET `/api/images*`) use `optionalAuth` middleware: if `auth.require_auth_for_browse` is false (default), unauthenticated requests pass through without a user context instead of returning 401. Authenticated users still get `is_owner` and other user-specific fields. All authenticated endpoints use the session cookie (not Bearer tokens). --- ## 6. Request Flow ``` Browser → SvelteKit SPA ↓ fetch() Go HTTP server ↓ middleware Auth check (session cookie validation — requireAuth or optionalAuth) ↓ Route handler ├─ db queries (SQLite) ├─ storage ops (disk read/write) └─ image processing (EXIF strip, thumbnail) ↓ JSON response → SPA renders ``` Browse endpoints use `optionalAuth` middleware: tries to authenticate from the session cookie but passes through without a user context on failure (unless `auth.require_auth_for_browse` is true, in which case it rejects like `requireAuth`). Mutating endpoints (upload, tag edit, logout, settings) always use `requireAuth` which rejects unauthenticated requests with 401. --- ## 7. Admin CLI Commands ``` weimar serve --config /path/to/weimar.toml --admin-email user@example.com --admin-password s3cur3 weimar users list --config /path/to/weimar.toml weimar users create --config /path/to/weimar.toml weimar users delete --config /path/to/weimar.toml weimar users password-reset --config /path/to/weimar.toml weimar image delete --config /path/to/weimar.toml ``` All commands require `--config` (or `WEIMAR_CONFIG` env var). --- ## 8. Image Storage ### Path Structure ``` {storage_path}/ab/cd/ef/. ``` - First 6 hex chars of SHA-256 of file content → 3 levels of 2 chars - Prevents thousands of files in a single directory ### Processing Pipeline 1. Validate file type (magic bytes, not extension) 2. Check file size against config limit 3. Generate SHA-256 hash for storage path 4. Strip EXIF metadata (images only) 5. Generate thumbnail: - Images: resize to configurable max dimension (default 300px) - Video: `ffmpeg -i -vframes 1 ` (skip if ffmpeg not found) 6. Write original + thumbnail to disk (hash-partitioned paths) 7. Insert metadata + tags into SQLite 8. Upsert tags into tag_usage table for autocomplete --- ## 9. Configuration (TOML) ```toml [server] host = "0.0.0.0" port = 8080 [database] path = "./data/weimar.db" [storage] path = "./data/images" [upload] max_size_mb = 50 rate_limit_per_minute = 0 # 0 = unlimited [auth] allow_registration = true # false = gated, admin creates users only require_auth_for_browse = false # true = unauth users blocked from browsing ``` --- ## 10. Session Management - **Login**: verify password → generate random 64-char hex token → store in `sessions` table → set `httpOnly` cookie named `weimar_session` - **Auth check**: read cookie → look up token in SQLite → check `expires_at` → attach user context to request - **Expiry**: sessions expire after 30 days of inactivity (reset on each request) - **Logout**: delete session from DB + clear cookie --- ## 11. Thumbnail Strategy - **Images**: server-side resize via Go `image` package (nearest-neighbour or bilinear). Store as JPEG. - **Videos**: shell out to `ffmpeg -i {input} -vframes 1 -s 300x300 {output.jpg}`. If ffmpeg not found on `$PATH`, store `null` in `thumbnail_path`. SvelteKit handles missing thumbnail gracefully. - **Format**: Thumbnails always stored as JPEG regardless of source format. --- ## 12. Tag System - **Input**: user types comma-separated tag string on upload - **Normalization**: lowercase, trimmed, whitespace-collapsed - **Autocomplete**: `GET /api/tags/suggest?q=fun` → `SELECT tag FROM tags WHERE tag LIKE 'fun%' ORDER BY usage_count DESC LIMIT 10` - **No collision resolution**: `funny-cat` and `funny_cat` and `funnycat` are distinct tags - **Deletion**: when an image is deleted, decrement `usage_count` for its tags; remove tag row if count reaches 0 --- ## 13. Key Design Decisions (vs Chevereto) | Concern | Chevereto V4 | weimar | |---------|-------------|--------| | Database | MySQL/MariaDB | SQLite | | Deployment | Docker / VPS / cPanel | Single binary | | Admin UI | Full web dashboard | CLI only | | Multi-tenancy | Yes (Traefik-based SaaS) | No | | Storage backends | Local + S3 + S3-compatible | Local filesystem only | | Image sizes | Original + thumb + medium | Original + thumb only | | Cache | Redis + file-based | None | | Job queue | Yes (cron-driven) | No | | Moderation | Flag-based + external services | None | | Video thumbnails | php-ffmpeg (required) | ffmpeg (optional) | | Mail | Symfony Mailer (15+ transports) | None | | 2FA | Google2FA | None | | Rate limiting | Flood protection | Configurable, off by default | | API auth | JWT + API Keys + signed requests | Session cookie only | | Language | 30+ languages | Single language (English) | --- ## 14. Out of Scope (Future Considerations) - Search beyond tag filtering (full-text search) - Albums / collections - User profiles - Comment system - Federation (ActivityPub) - S3/remote storage backends - Multi-architecture builds (x86_64 only is fine) - Prometheus metrics - OAuth / social login - Webhook notifications