SvelteKit hook + new DB table + founder-gated API + UI section. Ships
the code path for public-site routing on {slug}.mana.how and custom
hostnames. Cloudflare SaaS Hostnames integration is stubbed — see
plan §M6 "Offene Enden".
apps/api/src/modules/website:
- schema.ts: new `customDomains` table. Fields: id, site_id, hostname
(unique), status (pending | verifying | verified | failed),
verification_token, dns_target, verified_at.
- drizzle/website/0002_custom_domains.sql: manual migration with
partial unique index on (hostname) WHERE status='verified'.
- domains.ts (new, authenticated + founder-gated via
`requireTier('founder')`): POST/GET/DELETE /sites/:id/domains,
POST /sites/:id/domains/:domainId/verify. Verify runs CNAME + TXT
checks via node:dns/promises with an apex-domain A-record fallback.
Reserved-hostname list prevents users from binding mana.how subdomains.
- public-routes.ts: new GET /public/resolve-host?host= — unauthenticated
resolver used by hooks.server.ts. Returns { slug, siteId } only for
verified bindings tied to a currently-published site.
apps/mana/apps/web/src/hooks.server.ts:
- After the existing https/app-subdomain guards, a new
`resolveWebsiteRewrite()` step rewrites `event.url.pathname`:
{slug}.mana.how/path → /s/{slug}/path (pure string)
custom-host.com/path → /s/{resolved}/path (API call, 60s LRU)
- Browser URL stays on the custom host — this is a server-side rewrite,
not a 302. APP_SUBDOMAINS + RESERVED_WEBSITE_SUBDOMAINS win over
website routing. Localhost and apex mana.how are skipped.
apps/mana/apps/web/src/lib/modules/website:
- domains.ts (new): typed client for list/add/verify/remove. Handles
200 + expected 400 (verification-failed) separately.
- components/DomainsSection.svelte: add-input, per-domain status pill,
DNS-instructions box (CNAME + TXT with copy-to-clipboard), Verify
button. Mounted inside SiteSettingsDialog as its own section — the
existing theme/footer controls stay put.
docs/plans/website-builder.md:
- M6 checklist updated with what shipped vs. ops-gap (CF SaaS).
- `mana-landing-builder` consolidation: DECIDED to keep parallel. Four
reasons in the plan. Revisit-criterion stated.
- Shipping log table seeded with M1→M6 commits.
Validation:
- pnpm run validate:all: 6/6 gates green
- pnpm run check (web): 0 errors, 0 warnings
- apps/api type-check: green
Apply schema with:
psql "$DATABASE_URL" -f apps/api/drizzle/website/0002_custom_domains.sql
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two new block types and the server-side infrastructure for
untrusted input + cross-module data embedding.
Forms:
- packages/website-blocks/src/form: declarative fields (text, email,
tel, url, textarea, number) with required / maxLength / placeholder
per field. Honeypot hidden input in the renderer; public-mode POST
to a same-origin SvelteKit proxy that forwards to mana-api.
- apps/api: website.submissions table (schema.ts + 0001_submissions.sql)
+ POST /public/submit/:siteSlug/:blockId. Loads the current published
snapshot, finds the form block, validates payload against its
declared fields (trim, type check, length cap), rejects honeypot
submissions silently, rate-limits per IP (10 / 5 min) in-memory.
Unknown keys are dropped — clients can only submit declared fields.
- Owner-facing: GET/DELETE /sites/:id/submissions + SubmissionsView
component + /(app)/website/[siteId]/submissions route. Shows
incoming submissions with status pill + payload preview + delete.
- apps/mana/.../routes/s/[siteSlug]/__submit/[blockId]/+server.ts:
same-origin proxy so form posts don't trigger CORS and IP / user-
agent headers are forwarded via SvelteKit's trusted getClientAddress.
M4 first-pass does NOT wire target-module delivery (contacts / notify).
Submissions stay in the inbox until owner-side tool handlers land
(M4.x). `target` enum is intentionally `['inbox']` only for now.
moduleEmbed:
- packages/website-blocks/src/moduleEmbed: source dropdown
(picture.board | library.entries), max-items, layout (grid | list),
optional filter object. The `resolved` field on props is populated at
publish time by the editor-side resolver — public renderer reads it
directly, no Dexie / API round-trip needed.
- apps/mana/.../website/embeds.ts: per-source resolvers. picture.board
enforces `isPublic=true`; library.entries respects filter.isFavorite
/ kind / status so owners can expose a subset (e.g. "my favorites").
- buildSnapshot() walks the tree after assembly and fills in
block.props.resolved for every moduleEmbed. Publish slower, public
visits fast. No cross-service call at render time.
Validation:
- pnpm run validate:all: 6/6 gates green
- pnpm run check (web): 0 errors, 0 warnings
- apps/api type-check: green
Apply Postgres with:
psql "$DATABASE_URL" -f apps/api/drizzle/website/0001_submissions.sql
Plan: docs/plans/website-builder.md (M4 shipped)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Enables the M1 parallel-reads optimisation on the webapp side. Both
consumers of runPlannerLoop pass an isParallelSafe predicate derived
from the tool catalog:
isParallelSafe: (name) =>
AI_TOOL_CATALOG_BY_NAME.get(name)?.defaultPolicy === 'auto'
Auto-policy tools (list_tasks, get_habits, nutrition_summary, …) run
via Promise.all in batches of 10 when the LLM fans them out in one
round. Propose-policy tools — which surface to the user as Proposal
cards — stay sequential so intent ordering in the inbox is preserved
and pre-execute guardrails can reason about prior-step state.
Tests: 31 existing companion + mission tests pass unchanged; the
parallel path is exercised via the new loop.test.ts cases shipped
with the M1 commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end deep-research feature for the questions module: a fire-and-
forget orchestrator in apps/api that plans sub-queries with mana-llm,
retrieves sources via mana-search (with optional Readability extraction),
and streams a structured synthesis back to the web app over SSE.
Backend (apps/api/src/modules/research):
- schema.ts: pgSchema('research') with research_results + sources
- orchestrator.ts: three-phase pipeline (plan / retrieve / synthesise)
with depth-aware config (quick=1×, standard=3×, deep=6× sub-queries)
- pubsub.ts: in-process event bus, single-node, swappable for Redis
- routes.ts: POST /start (202, fire-and-forget), GET /:id/stream (SSE),
POST /start-sync (test only), GET /:id, GET /:id/sources
- Credit gating via @mana/shared-hono/credits — validate up-front,
consume best-effort on `done`. Failed runs cost nothing.
Helpers (apps/api/src/lib):
- llm.ts: llmJson() + llmStream() over mana-llm OpenAI-compat API
- search.ts: webSearch() + bulkExtract() over mana-search Go service
- responses.ts: shared errorResponse / listResponse / validationError
Schema deployment:
- drizzle.config.ts (research-scoped) + drizzle/research/0000_init.sql
hand-authored migration, deployable via psql -f or drizzle-kit push.
- drizzle-kit added as devDep with db:generate / db:push scripts.
Web client (apps/mana/apps/web/src/lib/api/research.ts):
- Typed start() / get() / listSources() / streamProgress(). The stream
uses fetch + ReadableStream (not EventSource) so we can attach the
JWT via Authorization header. Special-cases 402 for friendly toast.
- New PUBLIC_MANA_API_URL plumbing in hooks.server.ts + config.ts.
Module store (modules/questions/stores/answers.svelte.ts):
- New write-side store with createManual / startResearch / accept /
softDelete. startResearch creates an optimistic empty answer, opens
the SSE stream, debounces token deltas in 100ms batches into the
encrypted local row, and on `done` replaces the streamed text with
the parsed { summary, keyPoints, followUps } payload + citations
resolved against research.sources.id.
Citation rendering (modules/questions/components/AnswerCitations.svelte):
- Tokenises [n] markers in the answer body into clickable pills with
hover popovers showing title / host / snippet / external link.
- Lazy-loaded via a session-scoped source cache (stores/sources.svelte.ts)
that deduplicates concurrent fetches.
UI (routes/(app)/questions/[id]/+page.svelte):
- Recherche card with three-state button (start / cancel / re-run),
animated phase indicator, source counter.
- Confirmation dialog warning about web/LLM transmission since the
question itself is locally encrypted.
- Toasts for success / error / cancel via @mana/shared-ui/toast.
- Re-run flow soft-deletes prior research-driven answers but keeps
manual ones intact.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>