Backlog
Living list of things the product needs. Ordered roughly by impact. Shipped items move to "Recently shipped" at the bottom.
Loops to close — status
The "loop-incomplete, not feature-incomplete" framing held up. All three primary loops now close with visible receipts.
| Loop | Status | Notes |
|---|---|---|
| 1 — "I have a site" | ✅ done | Compile + S3 + CloudFront + ACM + Route53 all live. Build-on-publish per env. Auto-invalidation. |
| 2 — "I can publish content" | ✅ done | Media uploads land in S3, served via CloudFront at media.preview.staticowl.com. Picker wired. Publish-moment modal with live URL. |
| 3 — "I can trust it" | ✅ done | Drafts route, scheduler worker, version history modal with restore, publish receipts persisted on BuildEvent nodes. |
| Continuous AI guide | ✅ done | Shell-level Guide button; system prompt detects SETUP vs REFINE mode based on site state. |
Closure-with-receipts principle is still the lens. Every new surface needs immediate (toast/modal) + persistent (a settings row, an activity log entry, a status field). Don't ship silent success.
Next up (pick one and say go)
Each item is ordered inside so a fresh session can pick it up without archaeology. "Touches" lists files/AWS resources so the scope is transparent.
-1. Artifact pipeline ops (next session)
Architecture is locked. The build pipeline writes content-addressed
artifacts to S3 + sharded manifests when STATICOWL_ARTIFACTS_BUCKET
is set. The Lambda@Edge resolver code is bundled in
packages/build/src/aws/lambda-edge-resolver.js. Next session needs
to do the actual ops:
- Provision
staticowl-artifacts,staticowl-manifests,staticowl-buildlogsS3 buckets in us-east-1. - Lifecycle policies: artifacts intelligent-tiering, manifests retention 90d, buildlogs 30d.
- Deploy Lambda@Edge resolver, attach to CloudFront distribution as origin-request trigger. (Region must be us-east-1 — L@E requirement.)
- Set
STATICOWL_ARTIFACTS_BUCKETenv on EC2 to enable the pipeline in compile.ts. - Smoke test: trigger a build, verify pointer + shards + artifacts appear; hit the CDN; verify response.
- Determinism test: run the same Release through compile.ts twice,
feed both outputs into
compareSnapshots(), verify zero divergence.
After that lands, the architectural promise ("atomic blue/green deploys, content-addressed artifacts, full replay") is operational end-to-end.
0. AI co-author trio (deferred — architecture took priority)
Three connected plays that complete the Voice Profile loop. Order matters — auto-learn first (unlocks the others by lowering setup cost).
0a. Voice auto-learn (~half day)
Drop the editor's setup time to zero. Pick 5 best posts → AI infers styleGuide + toneTags + vocabulary.use/avoid → editor reviews and accepts. Today the Voice Profile UI is a blank textarea staring at people.
POST /api/agent/voice/infer— accepts{ sampleIds: string[] }, fetches the bodies, asks Claude to produce aVoiceProfileJSON. Returns the proposal (NOT auto-saved).- UI: in Settings → Voice tab, add a "Pick 5 posts and infer" button next to the styleGuide textarea. Picker shows recent published items; submit → modal with the inferred profile + accept button → fills the form fields (still editable).
- Touches:
packages/server/src/routes/agent.ts,packages/ui-next/public/js/views/settings.js.
0b. Localization fanout (~1 day)
Fills A26 (per-locale variants) from 6 → 9. Builds on the existing Locale model + bulk-rewrite infra + Voice Profile.
POST /api/agent/translate-fanout—{ type, id, targetLocales: string[] }. For each target locale, runs Claude with the source text + Voice Profile + a "translate tomatching our voice; keep brand terms in vocabulary.useuntranslated" instruction. Creates a sibling content item with the locale-stamped slug (matches the existing "Translate to…" duplicator output, but for ALL configured locales in one go).- UI: button on the content editor head — "Fanout to all locales". Confirmation modal listing the configured locales, with checkbox per-locale. Progress bar, per-row success/fail.
- Touches:
packages/server/src/routes/agent.ts,packages/ui-next/public/js/views/content-editor.js.
0c. AI co-author in editor (~2-3 days, the headline)
Cursor for content. As the editor types, AI watches the cursor + surrounding text + Voice Profile, offers ghost-text completions; Tab to accept, Esc to dismiss. Editor.js plugin + debounced background fetch.
POST /api/agent/inline-complete—{ before: string, after: string, fieldName?: string }. Returns{ completion: string }— short (1–3 sentence) continuation in the site's voice. Throttled per-keystroke (debounce ~600ms; cancel on next keystroke).- Editor.js plugin OR a custom block inline behaviour. Render the completion as ghost-text (CSS opacity 0.4) inside the active block; Tab → insert + advance cursor; Esc / next keystroke → discard.
- UX subtleties: never trigger inside code blocks; respect
voiceProfile.enabled(don't auto-complete if voice disabled); per-user "AI suggestions" toggle in Settings → AI defaults so editors can turn it off. - Touches:
packages/server/src/routes/agent.ts,packages/ui-next/public/js/views/content-editor.js, possibly a newpackages/ui-next/public/js/util/editorjs-cocomplete.js.
Why this trio together: 0a unlocks 0b and 0c (both lean on a real Voice Profile). 0b uses 0c's infra path. Skipping 0a means most users will never set up Voice and the rest underperforms. Ship them in sequence; 0a + 0b is a single day, 0c follows.
1. Themes catalogue (biggest product bet)
Built-in theme gallery + AI awareness + swap action. The AI guide already proposes a mantra + theme colours, but users have no way to try on a pre-made aesthetic or change theme without losing content.
- Theme spec (new file
packages/core/src/themes/types.ts):{ id, name, description, colors: {primary, accent, ink, surface, ...}, typography: {heading, body}, templates: [{boundType, html}], sampleFrame: {...} }. - Built-in themes — start with 4:
editorial,minimal,warm-blog,studio-portfolio. Source of truth:packages/core/src/themes/builtin/<id>.ts. - Theme apply — new endpoint
POST /api/sites/:id/theme/apply { themeId }. Upserts all templates from the theme (overwriting same-name), patchessite.theme.primary/accent. Does NOT touch content. - AI awareness — extend
SITE_GUIDE_SYSTEMprompt with the theme list and let the guide proposetheme: { id: 'editorial' }inside the plan. Apply-plan honours it. - Swap UI — new Themes view or a card in Site Settings. Grid of theme previews (screenshot or rendered sample), hover → "Apply to this site" button. Confirmation dialog since templates are replaced.
- Touches:
packages/core/src/themes/,packages/server/src/routes/sites.ts,packages/ui-next/public/js/views/themes.js(new),packages/ui-next/public/js/views/sites.js(settings card),packages/server/src/routes/sites.ts(AI prompt).
2. utils.http + utils.env in the transform sandbox
Unblocks real onPublish webhooks. Today transforms can't hit the network or read secrets. Curated surface only.
utils.http.get(url, {headers, timeoutMs})andutils.http.post(url, {body, headers, timeoutMs})— wrap Nodefetch. Enforce: host must matchSTATICOWL_HTTP_ALLOWLIST(comma-separated, defaults to*for now but should be opt-in in prod); max response size 1MB; 10s timeout default.utils.env.get(name)— reads fromSTATICOWL_TRANSFORM_ENV_ALLOW(comma-separated list of allowed env var names). Returnsundefinedfor anything not on the allowlist.- Document both in the transform editor's field-hint.
- Touches:
packages/core/src/crud/transform-runner.ts, Transforms editor help text.
3. Activity log
Read-only "who did what, when" for the current site. Right now every meaningful action (publish, deploy, invite, role change) is stamped on a node somewhere — no consolidated view.
- Schema: new
ActivityEventnode with{id, siteId, actor, verb, subjectType, subjectId, summary, metadata, at}. Append-only. - Emitters: content create/update/publish, site create/update, invitation create/accept/revoke, member role change, build started/finished.
- Route:
GET /api/activity?limit=50&siteId=<id>returns latest events. - UI: new
/activityview OR a card on the dashboard. Grouped by day, with filters by verb and actor. - Touches: new
packages/core/src/crud/activity-service.ts, every route that mutates anything of note, a new UI view or dashboard card.
4. Typed SDK / codegen
Given a site's schema, emit a TypeScript client users drop into their Next/Astro/Vite frontend.
- Codegen endpoint:
GET /api/sites/:id/sdk.tsreturns a complete TS file: one interface per ContentType + one fetcher (getBlogPosts(opts),getBlogPost(slug)) using the public API. - Target form: a single file, no dependencies, types + functions exported. Callers paste into their repo.
- Keep in sync: re-run after schema changes. No npm publishing — manual copy is fine.
- Touches: new
packages/core/src/codegen/sdk.ts, new route inpackages/server/src/routes/sites.ts.
5. Multi-env promote tool ✅ SHIPPED (architecture phase 3)
The Release/Deployment model makes this trivial: same Release object
deploys to staging then prod. UI is the per-env selector on Release
detail. No envPublishedAt_<toEnv> copying — Deployments point at
the same Release. See /admin/#/releases/:id.
For BULK env-snapshot promote (an entire env's state in one go), the Snapshot mechanism (ADR-0019) supports it; UI surface for "promote everything in dev → staging" is a future polish item.
6. i18n surface
LocaleService is stubbed. Content is single-locale. Turn it on.
- Per-site
locales: string[](e.g.['en', 'fr']) with onedefault. - Content item gains
translations: Record<localeId, {title, body, ...}>— fields on the node for default locale, JSON for overrides. - Editor: a language switcher in the top bar. Switching populates the form with the locale's fields; save writes only the override, default stays.
- Build emits per-locale subpaths:
/fr/blog/hello-world/. - Touches: ContentService extensive changes, compiler path logic, editor state + UI.
7. Notifications (in-app)
Bell icon in the shell header with a dropdown showing recent events relevant to the current user: invitation accepted, build failed, publish scheduled and fired, etc.
- Same underlying
ActivityEventnode as (3) with atoUserIdrecipient filter. - Unread count badge; mark-read endpoint; persistent (events don't vanish on reload).
- Touches: builds on (3); new shell widget; new routes for list + mark-read.
8. Backups UI
Engine has per-graph backup/restore (POST /graphs/:name/backup, /restore). UI exposes none of it.
- Add a "Backups" tab on Site Settings.
- Shows the list of stored backups, their sizes and timestamps; buttons for "Backup now", "Restore this", "Delete".
- Touches: new route that proxies to engine, new UI tab.
9. Schema polish
The Types editor works for basic fields but several things are rough.
- Relationship UI — target-type picker populated from existing types, cardinality selector (one/many), cascade-on-delete option. Today it's a free-text input for target.
- Field validators — regex, min/max for numbers, min/max length for strings, pattern hints.
- Nested schemas — support fields that are objects (e.g.
hero: {headline, subhead, image}) instead of flat-only. - Touches:
packages/ui-next/public/js/views/types.js,packages/core/src/crud/schema-service.ts(validators).
10. Site Settings tabbed page
The current modal covers name/URL/mantra/colours — fine for quick edits. A full page gives room for Domain, Build config, Members, API keys scoped to site, Backups, Danger zone (delete + transfer).
- New route
/sites/:id/settingswith tabs: General / Publishing / Members / API / Backups / Danger. - Each tab reuses existing endpoints; nothing new backend-wise except delete-site confirmation that actually drops the graph.
- Touches: new
packages/ui-next/public/js/views/site-settings.js; minor wiring.
Smaller polish wins (grab bag)
- Drag-drop reorder in content tree (Move-To works; drag-drop is smoother).
- Bulk operations on content list (select many → delete / publish).
- Duplicate-content action (one-click clone with new slug).
- Mobile preview viewport switcher in the preview modal.
- Unsaved-changes guard on editor nav-away.
- Loading states on all buttons that fire requests.
- Command palette content search.
- Keyboard shortcuts doc + more shortcuts.
- Dark-mode audit on the editor.
- Mobile layout for tree + editor split (collapses poorly under 900px).
Nice-to-haves / speculative
- Theme marketplace (post-catalogue).
- Form builder (contact / signup → submissions as content).
- Comments / reactions.
- First-party analytics (Plausible / Fathom wiring).
- Client-side search on rendered sites (Pagefind).
- E-commerce primitives.
- AI moderation at save time.
- Visual template builder.
Handoff context
Everything below is what a fresh session needs to pick this up without re-discovery. Read this first before starting any backlog item.
AWS resources (already provisioned, us-east-1)
| Resource | Identifier | Purpose |
|---|---|---|
| S3 bucket | staticowl-sites |
Site builds (<slug>/<env>/...) + media (_media/<siteId>/...). Versioning on. Public ACLs blocked. |
| CloudFront distribution | E3OEFOC02CNC4A (d1739x2ab831n0.cloudfront.net) |
Serves everything. HTTP/2+3, compression, redirect-to-HTTPS. |
| CloudFront OAC | E1IFGQT7QA43Q7 |
Only principal the bucket policy allows. |
| CloudFront Function | staticowl-rewrite |
Host-based routing + /_media/ bypass + / → /index.html. Source checked into chat history. |
| ACM cert | arn:aws:acm:us-east-1:033776104880:certificate/e5676873-0ad9-4415-a610-50cde4937e20 |
Wildcard *.preview.staticowl.com, DNS-validated. |
| Route53 zone | Z00980471QQ166640MN6O (staticowl.com.) |
*.preview.staticowl.com A+AAAA aliased to the distribution. |
| EC2 instance profile | graphiquity-engine-profile / role graphiquity-engine-role |
Inline policy StaticOwlDeploy grants s3:PutObject/GetObject/DeleteObject/ListBucket on the bucket + cloudfront:CreateInvalidation. |
| EC2 instance | i-078edfedf02808d7a |
98.83.66.102. Runs the engine + CMS. |
Custom domains per site are Phase-2 — would need a per-site CF distribution + ACM cert provisioned via API.
Environment variables the CMS reads
| Var | Default | Notes |
|---|---|---|
GRAPHIQUITY_ENDPOINT |
http://localhost:3001 |
Engine writer. All CMS graph calls go here. |
GRAPHIQUITY_ENGINE_SECRET |
(set in ecosystem.config.cjs) | Shared secret — HMAC signs every request. |
STATICOWL_BUCKET |
staticowl-sites |
Site-build + media bucket. |
STATICOWL_REGION |
us-east-1 |
Bucket region. |
STATICOWL_PREVIEW_HOST |
preview.staticowl.com |
Used to compute publicUrl as <slug>-<env>.<host>. |
STATICOWL_MEDIA_HOST |
media.preview.staticowl.com |
Prefix for uploaded media URLs. |
STATICOWL_CF_DISTRIBUTION |
E3OEFOC02CNC4A |
Default distribution to invalidate after a deploy. |
STATICOWL_BUILD_DIR |
/opt/staticowl/builds |
Where the compiler writes HTML before S3 upload. |
STATICOWL_SCHEDULER |
(unset → on) | Set to off to disable the in-process scheduler. |
ANTHROPIC_API_KEY |
(set in ecosystem) | AI guide uses Sonnet 4.6 with Haiku 4.5 fallback. |
File layout (CMS)
packages/
core/src/crud/
platform-service.ts ← Site, SiteEnvironment, PublishConfig, DEFAULT_ENVIRONMENTS
content-service.ts ← publishToEnv, unpublishFromEnv, listDrafts, getHistory, restoreAt
transform-service.ts ← recordRun, listRuns
transform-runner.ts ← node:vm sandbox (add utils.http/env here)
starter-kits.ts ← Persona → types + sample content
server/src/
build/compile.ts ← walks content, renders via render.ts, writes HTML
build/deploy.ts ← S3 sync + CF invalidation
build/render.ts ← shared with preview endpoint (identical HTML)
routes/build.ts ← POST /api/build, /build/last, /build/history
routes/content.ts ← tree + preview + history + publish + unpublish + restore
routes/sites.ts ← AI guide chat/apply + reseed + theme swap (TODO)
routes/transforms.ts ← CRUD + /_run + /:id/run + /:id/runs
scheduler.ts ← in-process 60s worker for publishAt_<env>
ui-next/public/js/
shell/shell.js ← NAV, persona filtering, Guide button
shell/onboarding.js ← persona picker
shell/bug-reporter.js ← bug/feature/question submit
views/content-tree.js ← /content
views/content-editor.js ← /content/new, /content/:type/:id
views/drafts.js ← /content/_drafts (must load before content-list.js)
views/builds.js ← /builds, /builds/:id
views/transforms.js ← /transforms editor + Recent runs tab
views/site-guide.js ← /sites/:id/guide (chat + plan pane)
views/sites.js ← /sites (list + create modal + settings modal)
Key naming conventions (don't break these)
- Site S3 prefix is
slug(not siteId, which contains:). - Content items carry
envPublishedAt_<envId>timestamp +envPublishedBy_<envId>user id per env they're published to. - Scheduled publishes use
publishAt_<envId>— set by UI, cleared by the worker on fire. :ContentItemis a shared secondary label on all content items — used for cross-type tree queries.- Route order matters for
/content/*: drafts.js (for_drafts) loads before content-list.js (which grabs:type). /_media/URIs are the CloudFront function's bypass path — DO NOT rename without updating the function.
Deploy
- CMS:
bash deploy-cms.sh— tars packages/*/dist + package-lock.json, uploads to S3, SSM reloads pm2. - Engine (separate concern):
bash deploy-engine.sh. - Infra (S3/CF/ACM/Route53): already provisioned. Per-site custom domains would need a provisioning script — defer.
Current scaffolding worth calling out
- Scheduler runs in the same Node process as the CMS (single-instance). For multi-CMS-instance future, need external locking (Redis SETNX or a DynamoDB row with a TTL).
- Transform sandbox has no network / env access yet — item (2) in the Next-up list.
publicUrlis always the preview-domain form (<slug>-<env>.preview.staticowl.com). Custom domains are stored on the Site but not actually provisioned.
Recently shipped (newest first)
2026-04-26 — Versioned site compiler architecture (multi-push)
The biggest architectural shift in the project to date. The CMS is now a versioned site compiler underneath the editorial UI.
ADRs locked (in docs/adr/):
- ADR-0018 System Model — Site = f(env, time)
- ADR-0019 Deployment Fact Model — append-only Deployments
- ADR-0020 Versionable Scope — what's bundled in a Release
- ADR-0021 Review Model — polymorphic Reviews with deploy gate
- ADR-0022 Selector / Relationship Unification — manual + query templates
- ADR-0023 Artifact + Manifest Architecture — content-addressed, sharded, atomic
Core services shipped:
ReleaseService— Environment, Deployment, Release, Snapshot with state machineReviewService— polymorphic Review + deploy gate algorithm- Manifest helpers — content-addressing, sharded manifest, S3 keys, xxhash32
- Selector resolver — six query template kinds with deterministic ORDER BY
- Determinism harness —
compareSnapshots()for build verification - Migration planner — synthesise Deployments from legacy
envPublishedAt_<envId>stamps - Observability + DeploymentMode kill-switch (env-driven, defaults to
new)
Server routes:
/api/releases/*— full CRUD on Releases + Deployments + Replay- Site Health endpoint accepts optional
releaseId→ persists findings as a Release-scoped Review - Deploy endpoint runs
deployGate()before recording the Deployment fact (blocking findings → 409)
UI surfaces:
/admin/#/releases— list + named Release builder/admin/#/releases/:id— Release detail with state transitions, scheduled deploy, rollback/admin/#/replay— date+env picker → resolved active Release (the demo killer)/admin/#/audit— composes existing graph queries into the compliance dashboard
Build pipeline:
compile.tsreads from Deployment facts innewmode (default); legacy fallback retained behind kill-switchArtifactWritercontent-addresses outputs, builds sharded manifests, atomic pointer flip — opt-in viaSTATICOWL_ARTIFACTS_BUCKETenv- Aggregate dependency tracking (
packages/build/src/aggregate-dependencies.ts) wires selectors to invalidator
Lambda@Edge resolver:
packages/build/src/aws/lambda-edge-resolver.js— production-ready, self-contained, pointer + shard caching- Bundled in repo; AWS provisioning is the next ops task
Test coverage: 242/242 passing across 24 test files. New code paths in core (ReleaseService, ReviewService, manifest, selectors, observability, determinism, migration) all unit-tested with mock graph client.
Marketing: docs/positioning.md rewritten for three audiences
(editor / engineer / compliance). Four killer demos enumerated.
2026-04-22 — phase 1 Loops + observability
- Loop 1: Compiler, S3 sync, CloudFront + ACM + Route53 wildcard, CF Function for host-based routing.
- Loop 2: Media pipeline on S3 via
media.preview.staticowl.com; picker wired to Editor.js image tool. - Loop 3: Drafts route, publish-moment modal with live URL, scheduler worker for
publishAt_<env>, version history modal with engine-backed restore. - Continuous AI guide: Shell Guide button; REFINE vs SETUP mode in the system prompt based on existing site state.
- Transform observability:
TransformRunpersistence; Recent runs tab on transform editor. - Publish receipts everywhere: BuildEvent persists
publicUrl; /builds shows history with drill-in; publish modal shows the live URL; invalidation runs automatically. - Fixes: persona-reload cache-bust, bug / feature / question picker in the reporter,
/api/auth/users+/api/auth/api-keyswired,/api/content/_all|_recent|_counts|_drafts, ContentService array-unwrap, favicon.
Earlier (Phase 0)
See docs/positioning.md + docs/user-stories.md for the before-state and the competitive comparison.