aboutsummaryrefslogtreecommitdiffstats
path: root/AGENTS.md
diff options
context:
space:
mode:
Diffstat (limited to 'AGENTS.md')
-rw-r--r--AGENTS.md55
1 files changed, 45 insertions, 10 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 3a1da81..8b997e4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -23,6 +23,9 @@ table and deployment notes.
(e.g. the `<noscript>` button in `posts.html`).
- Server-rendered `html/template`; no SPA, no frontend toolchain.
- Postgres holds **everything**, including images (`images.data bytea`).
+ **Each blog is its own database** (`blog_<sub>`, see Key mechanics) so a
+ blog is backed up and restored with plain `pg_dump`/`psql`; the control
+ database (`DATABASE_URL`) holds only `users` and the `blogs` registry.
- Posts are Markdown (goldmark → bluemonday). No WYSIWYG. The editor's
"Insert image" is the one scripted convenience (`partials/editor.html`);
it must keep its no-JS fallback (file appended on save).
@@ -40,10 +43,14 @@ table and deployment notes.
## Layout
```
-cmd/blogspace/ main.go (serve|seed|migrate, superadmin + root blog bootstrap), seed.go
+cmd/blogspace/ main.go (serve|seed|migrate, superadmin + root blog bootstrap,
+ migrates every blog database at start), seed.go
internal/config/ env → Config; RootSubdomain = "www"
-internal/db/ pgxpool + goose; migrations/*.sql embedded
-internal/store/ models + hand-written SQL (users, blogs, pages, posts, images, sections, modules, menu)
+internal/db/ Cluster (control pool + lazy per-blog pools, CREATE/DROP DATABASE),
+ goose providers; migrations/control/*.sql, migrations/blog/*.sql,
+ split.go (control Go migration 00006: the one-off move to per-blog DBs)
+internal/store/ Store = control DB (users, blog registry, create/delete blog);
+ BlogStore = one blog's DB (settings, pages, posts, images, sections, modules, menu)
internal/auth/ bcrypt, JWT issue/parse, cookie, HMAC CSRF
internal/markdown/ Render(md) → sanitized HTML
internal/slug/ Make/Valid/WithSuffix (Greek transliteration included)
@@ -57,11 +64,29 @@ internal/web/ server.go (host router, middleware, render helpers)
## Key mechanics
+- **One database per blog**: the registry row (`blogs`: id, owner, subdomain,
+ `db_name`) lives in the control DB; everything else — a one-row `settings`
+ table (title, tagline, theme) plus pages, posts, images, sections, modules,
+ menu_items — lives in `blog_<sub>` (`db.DBName`: dashes → underscores, so
+ subdomains are capped at 58 chars). No table in a blog DB carries a
+ blog id; the database is the scope. `store.Store` (control) hands out a
+ `store.BlogStore` per blog via `Store.Open`, which also fills `Blog.Title/
+ Tagline/ThemeJSON` from `settings`. `db.Cluster` caches one small pool per
+ blog (4 conns, idle closed after 2 min). `Store.Open` resets the pool and
+ retries once so a `dropdb --force` + restore under a running app is seamless.
+ Creating a blog: registry insert in a control tx → `CreateBlogDB` (CREATE
+ DATABASE + blog migrations, outside the tx) → settings/home page/defaults →
+ commit; any failure rolls back and drops the DB. A leftover database with
+ the blog's name is `db.ErrDatabaseExists` (409 in `/admin/users/new`).
+ Deleting a user drops their blog database. Start-up (`migrateBlogs`)
+ migrates every registered blog DB; a missing one is logged and skipped.
- **Host routing** (`web/server.go` `ServeHTTP`): base domain or `www.` →
root mux with `ctxHostSub = "www"`; `<sub>.base` → subdomain mux; anything
else 404. Public blog handlers get the blog via the `hostBlog` wrapper;
management handlers get it via `withBlog` (`/b/{sub}/…`, owner or
- superadmin). Both put it in `ctxBlog`; templates see it as `.Blog`.
+ superadmin). Both call `resolveBlog`, which puts the blog in `ctxBlog`
+ (templates see it as `.Blog`) and its `BlogStore` in `ctxBlogStore`
+ (`blogStore(r)` in handlers).
- **Root blog** is a normal `blogs` row with subdomain `www`, owned by the
first superadmin, created by `ensureRootBlog` on startup. Managed at
`/b/www/`. `Config.BlogURL("www")` returns the root URL.
@@ -141,7 +166,12 @@ internal/web/ server.go (host router, middleware, render helpers)
(`handleFavicon`) serves the png or redirects to the blog's image, so
icon-probing browsers never hit the 404 page; `favicon.ico` is reserved.
- **Images**: `/media/{uuid}` served on every host with immutable cache
- headers. Uploads are content-sniffed (png/jpeg/gif/webp/ico). Deleting an
+ headers, from the host's blog database only (the root domain serves the
+ root blog's images). Dashboard previews for another blog on the root host
+ therefore use `GET /b/{sub}/media/{id}` (`withBlog`): `dashboard/images.html`
+ and the `imagepick` partial (`sub` arg). Markdown keeps the relative
+ `/media/…` form because post bodies render on the blog host.
+ Uploads are content-sniffed (png/jpeg/gif/webp/ico). Deleting an
image clears theme references to it. `POST /b/{sub}/images/upload`
answers JSON (`{id, filename, markdown}` / `{error}`) when the request
has `Accept: application/json`; that is what the editor script calls.
@@ -172,7 +202,7 @@ internal/web/ server.go (host router, middleware, render helpers)
make dev # postgres in docker + app (DEV=1: templates/static reloaded from disk)
make seed # admin/admin (superadmin, owns root blog), alice/alicealice
make test # unit tests; no DB needed
-make db-reset # wipe dev data
+make db-reset # wipe dev data (control and all blog databases: the whole volume)
```
Port 8080 may be taken on the owner's machine: `make dev ADDR=:8090 PUBLIC_PORT=8090`.
Browsers resolve `*.blogspace.localhost` to loopback; with curl use
@@ -184,11 +214,16 @@ superadmin password to `admin`. Production refuses both.
## Conventions
- Run `gofmt -w .`, `go vet ./...`, `go test ./...` before committing.
-- Migrations: add `internal/db/migrations/NNNNN_name.sql` with goose
+- Migrations: `internal/db/migrations/blog/NNNNN_name.sql` for blog content
+ (runs in every blog database — it must not reference `users`/`blogs`) and
+ `internal/db/migrations/control/` for users and the registry; goose
`-- +goose Up/Down` sections; they run automatically at startup. Never
- edit an applied migration.
-- New store methods take `ctx` first and scope queries by `blog_id` so a
- blogger can never touch another blog's rows.
+ edit an applied migration. Control 00005–00007 are the one-off split (add
+ `db_name`, move content in `split.go`, drop the old tables) — a fresh
+ install still replays them.
+- Blog content goes through `BlogStore` methods, which take `ctx` first and
+ are bound to one blog's database, so a blogger can never touch another
+ blog's rows; users and the registry go through `Store`.
- Keep handlers thin: validate → call store → `redirectOK`. Re-render the
form with `"error"` in the data map on validation failure (4xx status).
- Match the existing comment density; comments explain *why*, not *what*.