We already did this once. AFFiNE runs as a mounted module inside the SISO client platform — same page, same login, its own Postgres schema. Here is the mechanism, the two public repos that carry it, and what Camron needs to copy.
Nothing needed unlocking. Both were made public on their last push and are live right now.
The composite app — AFFiNE frontend + backend assembled, plus our siso/
integration overlay. This is the one to read.
The pinned AFFiNE server: CRDT doc persistence, quotas, auth, file ingestion, NAPI native modules.
The pinned AFFiNE frontend fork. Included because siso-knowledge
references it as the frontend source.
Verified 13 Sep 2026. gh repo view returns
"isPrivate": false / "visibility": "PUBLIC" for all three. The
siso-knowledge description still reads "Private SISO Knowledge composite
application" — that string is stale text, not a visibility setting.
There is no Postgres rebase, because AFFiNE was never on anything else.
AFFiNE is natively Postgres + Prisma upstream. schema.prisma reads
provider = "postgresql" straight from the donor, with 116 Prisma
migrations retained unmodified.
So the question isn't "has it been migrated?" It's "how do two Postgres apps share one database without colliding?" That is the actual problem we solved, and the answer is schema isolation.
One Postgres instance. One browser page. One login. Four hard boundaries keep the donor app from touching the host's data.
| Layer | Isolation | Where it's declared |
|---|---|---|
| Postgres | Own schema knowledge, own least-privilege role. AFFiNE's 116 migrations run unmodified against ?schema=knowledge. | siso/migrations/knowledge.sql |
| Redis | Key prefix knowledge:, including Socket.IO pub/sub channels and mutex keys. | module-infra.json |
| Blobs / Yjs | Own S3 bucket knowledge, prefix knowledge/. | module-infra.json |
| Identity | Host signs a context header; module never authenticates. Fails closed. | siso/identity.ts |
-- siso/migrations/knowledge.sql
CREATE SCHEMA IF NOT EXISTS knowledge;
REVOKE ALL ON SCHEMA knowledge FROM PUBLIC;
GRANT USAGE, CREATE ON SCHEMA knowledge TO knowledge;
That's the trick. The donor app is untouched — it just gets pointed at a schema it owns alone, with a role that cannot read anything else.
The host mints an HMAC-signed context and passes it on every request as
x-siso-request-context. Express middleware validates it with a timing-safe
comparison and fails closed 401/403 on any of five conditions:
view / edit / share / adminAll five have negative tests. The module has no login screen of its own.
mount(target: HTMLElement, { host, backendBase? })
unmount() // or the function mount() returns
| Property | Value |
|---|---|
| Same document | yes — real DOM mount |
| iframe | no |
| Second login | no |
| Base path | /knowledge, configurable |
| Peer deps | react, react-dom, react-router-dom |
| Rail ownership | host owns global rail + tokens; module owns contextual nav |
Phase 3 passed with a live stack — not a design doc. Receipts are committed in
siso/verification/.
| Check | Result |
|---|---|
| Topology tests | PASS 3/3 |
| Signed-context auth tests | PASS 2/2 + 5 negative checks |
| Frontend typecheck | PASS |
| Rspack build | PASS — 848 assets, entry SHA-256 pinned |
| Runtime start / status / stop | PASS — frontend :3022, backend :3012 |
Native migrations on knowledge schema | PASS |
| Browser journey | PASS — create doc, rename, reload, restart both services, title + workspace retained |
| Donor surfaces | PASS — quick-search, favorites, collections, tags, edgeless canvas, database block, HTML export |
| Second-user browser isolation | NOT PROVEN — harness was deliberately single-user |
module-manifest.json — the whole contract on one page: entry point, identity model, capabilities, data authority. Start here.siso/migrations/knowledge.sql — the five-line schema boundary.siso/identity.ts — the identity interface the host must satisfy.siso/database-adapter.ts — the config seam; note it does no data migration.backend/.../auth/siso-request-context.ts + its .spec.ts — the signed-header middleware and its negative tests.siso/verification/one-shot-receipt.json — exactly what was proven and what wasn't.upstream-manifest.json — the pinned donor commits, fetch-only remotes.Licence — the backend is not MIT. AFFiNE is split-licensed.
packages/backend and packages/common/native fall under the
AFFiNE Enterprise Edition licence, which requires a paid subscription with
seat count for production use. Everything outside those directories is MIT.
Our integration mounts the frontend module and runs the backend locally, so this hasn't bitten us — but HALO is a commercial product for a real agency. Camron should decide deliberately: stay MIT-frontend-only, or buy EE seats. Worth ten minutes now rather than a surprise later.
Upstream pins are fetch-only by policy.
upstream-manifest.json declares "local-pins-only; upstream remotes are
fetch-only", and the push URLs are literally set to DISABLED so a push
fails at transport. Copy that habit — it's what stops an accidental push to a donor repo.
Pinned commits: frontend 837a5c2, backend 4b5d792.
HALO is Convex-backed and mid-migration off Supabase, so the host side differs. The transferable part is the pattern, not the plumbing.
module-infra.json assumes a shared
Postgres + Redis + S3 that HALO doesn't have yet. AFFiNE needs a real Postgres regardless —
it will not run on Convex. So HALO gains a Postgres dependency alongside Convex, which is a
genuine architecture decision, not a detail.