# 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 project `lycatoprod`; 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_path` with 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_CONFIGURED` and 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>` and
  `publicSites/<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_configuration` until 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:

1. The founder signs in only to Lycato Firebase. The Lycato server verifies the UID,
   tenant ownership, budget, permissions, and idempotency request.
2. 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.
3. Ujex delivers a one-time scoped agent credential directly to the selected
   runtime vault and returns only an opaque identity and credential reference.
4. 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>.Lycato` URL 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 serve` provides.
- 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:

1. SECURITY_MODEL + AUTH — without these, no secrets can be stored.
2. MULTI_TENANT (control plane) — gates per-tenant provisioning.
3. BILLING — gates monetisation. Self-host always free regardless.
4. BACKUPS — required before we promise SLAs.
5. CAPTURE_PIPELINE — last, since it depends on auth + storage.

## Cross-cutting principles

1. **Tenant isolation is physical, not logical.** One container,
   one volume, one DB per tenant. Easier audit, easier delete.
2. **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.
3. **No tenant-readable secrets.** Adapter keys never leave the
   server-side decrypt boundary. The cockpit displays "configured"
   / "not configured" only.
4. **Compliance posture from day one.** GDPR + SOC 2 Type I
   alignment from launch. Self-hosters keep their existing posture
   unchanged.
5. **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.
