Hosted Product Design (v0.4.0+)
Most of the hosted product remains design. The first P0 foundation now lives in
apps/hosted/: Firebase email/password and Google auth, verification/recovery, protected company metadata, and a retry-safe uid-keyed founder bundle. The bundle creates exactly one tenant, company, provisioning ledger, CRM default, consent-first analytics default, business-memory scaffold, obligation register, paused Operator roster, and company-presence draft. A founder can preview and publish immutable website versions, roll back, receive consented leads into CRM, and review PII-free first-party analytics on a temporary Hosting path. This bounded path is deployed on Firebase projectlycatoprod; its lifecycle is covered by emulator integration/browser suites, while a disposable production canary remains pending. Mail, customer-managed/platform hostname, repository, billing, executable hosted Operator work, and lifecycle remain pending or blocked as described below.
Implemented hosted foundation
All initial Firestore resources use the verified Firebase UID as their document id and are created in one transaction. The site draft is provisioned in a second retry-safe transaction after the founder bundle exists. Replays only create missing deterministic documents. Security rules keep the foundation records create-only, bind every private record and query to the verified owner, expose only an explicit published-site projection, and withhold the provisioning ledger until the complete internal bundle exists.
The stored ledger distinguishes provisioned defaults from external readiness:
- CRM pipeline, consent defaults, memory sections, Founding obligation, and three paused/zero-budget Operator roles are created.
- Analytics defaults use
collecting_fallback_pathwith explicit measurement provenance; consented page and conversion events work on the current/company/<uid>Hosting fallback while the future platform hostname stays pending. - Public site drafts include configurable SEO title, description, robots, and canonical fallback-path metadata on publish.
- Public leads create in-app founder notifications and accept GDPR-style lead deletion requests without inventing mail delivery.
- Pending team invites can be redeemed by verified invitees on
/accept-invite?tenant=<ownerUid>; hosted review/comment/approve UI remains pending. - Draft edits are private. Publish creates an immutable version; rollback moves the public projection to an existing version without deleting history.
- Public leads require response consent, record marketing consent separately, retain only bounded source attribution, dedupe the same email in the browser contract, and enter CRM with an explicit follow-up state.
- Analytics events accept a fixed schema without name, email, message, full URL, IP address, user agent, or device/session identifier.
- Honeypot, minimum dwell time, validation, immutable public writes, and dedupe
provide basic anti-abuse. Managed App Check/Turnstile attestation remains
BOT_ATTESTATION_NOT_CONFIGUREDand is required before claiming hardened bot protection. - Unauthenticated lead and analytics creates are metered by a per-site,
per-UTC-day intake counter at
publicSites/<uid>/leadIntake/<day>andpublicSites/<uid>/eventIntake/<day>. Every public create must advance its counter by exactly one in the same atomic commit, and the counter pins the id of the document it admits — so one increment cannot cover a batch of creates. Caps are 200 leads and 5000 analytics events per site per day. Counters are owner-read-only and today's window cannot be deleted, so the cap cannot be reset. Repeat submissions from the same email do not consume quota (they mint no new document). Accepted trade-off: a flood can spend a site's daily quota and degrade that day's form availability — a bounded, self-healing outcome in place of unbounded write-quota and billing abuse. - Operator execution is
pending_external_runtime; no Ujex identity or runtime is fabricated. - Repository provisioning is
pending_implementation. - Mailbox and platform hostname are
pending_configurationuntil Lycato domains are chosen.
The implemented Firebase foundation is logically tenant-isolated. It is not the future per-tenant compute/volume architecture described below and does not claim physical isolation yet.
What is in the repo but not in the product
Nothing. The unshipped/ tree that used to sit beside web/ — nine modules the
hosted app never imported (agent-roster.js, agent-trust.js, billing.js,
crm-ops.js, mailbox-mediator.js, margin-report.js, tenant-context.js,
tenant-health.js, tenant-saga.js, plus an ops-console/ shell) — was
deleted in the 2026-08-23 apps/ move (commit 65ee343). That removed the
plan-gate and billing subsystem, the resumable-provisioning saga, the company
switcher, the mailbox mediator and the margin report from the repo entirely.
Dated documents that still describe those modules as "present but unshipped" are
describing the pre-65ee343 tree; recover them from git history if they are
wanted back.
The real import graph is small: app.js reaches bootstrap, diagnostics,
entitlements, lifecycle, operator-admin, presence and roles; site.js reaches
bootstrap and presence. apps/hosted/test/shipped-graph.test.ts fails if an
unreferenced .js file appears in web/.
Operator access is decided in firestore.rules (isOperatorAdmin(), backed by
an operator_admin custom claim or an operators/{uid} document no client may
write) and is read-only. The browser-side elevate and tenant-isolation helpers
that used to live in operator-admin.js are deleted; a check running in the
tenant's own browser was never an access control.
Hosted Operator production boundary
The hosted browser cannot safely provision Ujex or a runtime directly. Lycato needs a server-side mediator before any roster entry may become executable:
- The founder signs in only to Lycato Firebase. The Lycato server verifies the UID, tenant ownership, budget, permissions, and idempotency request.
- That server calls a future Ujex OEM endpoint. Ujex remains the authority for agent identity, policy, approval, audit, and mailbox projection through Ruzayo. Lycato must not reuse the founder's Lycato token as a Ujex login.
- Ujex delivers a one-time scoped agent credential directly to the selected runtime vault and returns only an opaque identity and credential reference.
- The runtime receives Lycato's own scoped product credential server-to-server, registers/heartbeats with Ujex, and returns only an opaque runtime id.
As of 2026-07-23, Lycato has no server Function, Ujex has no OEM provisioning
endpoint with runtime-vault delivery, and no cloud runtime/vault provider has
been selected. Blaze billing does not fill those interfaces. The roster
therefore stays paused with zero budget and pending_external_runtime; the
browser never receives Ujex, runtime, or Ruzayo service credentials.
What the hosted product is
A managed, single-tenant-per-founder deployment of the Lycato cockpit backed by per-tenant encrypted volumes. The founder gets:
- A
<their-slug>.LycatoURL with auto-TLS. - Passkey or magic-link auth (no passwords).
- Encrypted secret store for adapter keys (KMS-backed).
- Scheduled syncs beyond the in-process loop
lycato serveprovides. - Managed daily backups to founder-owned S3 (BYO bucket).
- Inbound email and voice capture (Phase 10 hosted slice).
- Web push notifications for high-risk approvals.
What it is not: a multi-tenant SaaS. Each tenant gets isolated compute + storage. The "multi-tenant" we ship in v0.4.0 is operational multi-tenant (one control plane runs many single-tenant deployments), not request-multiplexed shared compute.
Pricing (intended, not chargeable today)
None of this is purchasable. The deployed product has exactly one plan,
founder, at £0: apps/hosted/web/entitlements.js reports
priceMonthlyGbp: 0, stripeConnected: false, checkoutAvailable: false, and
firestore.rules admits plan == 'founder' on tenant create and denies every
tenant update, so no founder can change plan. There is no Stripe integration
deployed and no server to receive its webhooks.
The intended hosted price is £39/month for one company. Nothing is charged before the founder sees and confirms the terms. The master brand defines no public feature ladder or team tier.
Self-hosting always free; AGPL ensures the open code stays open.
Docs in this directory
| File | Scope | ADR target |
|---|---|---|
SECURITY_MODEL.md |
KMS-backed secret store, encryption at rest | ADR-0025 (future) |
AUTH.md |
Passkeys + magic-link, session handling | ADR-0026 |
BILLING.md |
Stripe checkout, plan tiers, dunning | ADR-0027 |
MULTI_TENANT.md |
Single-tenant per founder; control plane | ADR-0028 |
CAPTURE_PIPELINE.md |
Postmark inbound email + Whisper voice + push | ADR-0029 |
BACKUPS.md |
Daily encrypted backup to founder-owned S3 + restore | ADR-0030 |
Sequencing
These docs are designed to be implemented in this order. Each block gates the next:
- SECURITY_MODEL + AUTH — without these, no secrets can be stored.
- MULTI_TENANT (control plane) — gates per-tenant provisioning.
- BILLING — gates monetisation. Self-host always free regardless.
- BACKUPS — required before we promise SLAs.
- CAPTURE_PIPELINE — last, since it depends on auth + storage.
Cross-cutting principles
- Tenant isolation is physical, not logical. One container, one volume, one DB per tenant. Easier audit, easier delete.
- Founder-owned data path stays git-native. Even hosted, the tenant volume is a git repo. We push it to the founder's GitHub on commit so the founder owns the canonical copy at all times.
- No tenant-readable secrets. Adapter keys never leave the server-side decrypt boundary. The cockpit displays "configured" / "not configured" only.
- Compliance posture from day one. GDPR + SOC 2 Type I alignment from launch. Self-hosters keep their existing posture unchanged.
- Hosted does not break self-host. Anything that requires the hosted control plane is opt-in. The CLI and cockpit run unchanged against a local company repo.