SISO · Integration one-pager · 13 Sep 2026

Putting AFFiNE inside another app

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.

01The two repos — both already public

Nothing needed unlocking. Both were made public on their last push and are live right now.

Public
sisodias/siso-knowledge

The composite app — AFFiNE frontend + backend assembled, plus our siso/ integration overlay. This is the one to read.

Public
sisodias/sisonotes-backend

The pinned AFFiNE server: CRDT doc persistence, quotas, auth, file ingestion, NAPI native modules.

Public
sisodias/siso-docs

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.

02The correction that saves Camron a week

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.

03How it works

One Postgres instance. One browser page. One login. Four hard boundaries keep the donor app from touching the host's data.

BROWSER — ONE PAGE, ONE SESSION Host app owns: router, global rail, design tokens, session issues x-siso-request-context Knowledge module mount(target, {host}) owns: docs, editor, canvas no iframe · no 2nd login mounts ONE POSTGRES — siso_platform schema: core host tables role: host schema: knowledge 116 AFFiNE migrations role: knowledge (least-priv) no cross-access

The four boundaries

LayerIsolationWhere it's declared
PostgresOwn schema knowledge, own least-privilege role. AFFiNE's 116 migrations run unmodified against ?schema=knowledge.siso/migrations/knowledge.sql
RedisKey prefix knowledge:, including Socket.IO pub/sub channels and mutex keys.module-infra.json
Blobs / YjsOwn S3 bucket knowledge, prefix knowledge/.module-infra.json
IdentityHost signs a context header; module never authenticates. Fails closed.siso/identity.ts

The whole schema boundary is five lines

-- 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.

Single sign-on, no second login

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:

All five have negative tests. The module has no login screen of its own.

The mount contract

mount(target: HTMLElement, { host, backendBase? })
unmount()   // or the function mount() returns
PropertyValue
Same documentyes — real DOM mount
iframeno
Second loginno
Base path/knowledge, configurable
Peer depsreact, react-dom, react-router-dom
Rail ownershiphost owns global rail + tokens; module owns contextual nav

04What was actually proven

Phase 3 passed with a live stack — not a design doc. Receipts are committed in siso/verification/.

CheckResult
Topology testsPASS 3/3
Signed-context auth testsPASS 2/2 + 5 negative checks
Frontend typecheckPASS
Rspack buildPASS — 848 assets, entry SHA-256 pinned
Runtime start / status / stopPASS — frontend :3022, backend :3012
Native migrations on knowledge schemaPASS
Browser journeyPASS — create doc, rename, reload, restart both services, title + workspace retained
Donor surfacesPASS — quick-search, favorites, collections, tags, edgeless canvas, database block, HTML export
Second-user browser isolationNOT PROVEN — harness was deliberately single-user

05Read it in this order

  1. module-manifest.json — the whole contract on one page: entry point, identity model, capabilities, data authority. Start here.
  2. siso/migrations/knowledge.sql — the five-line schema boundary.
  3. siso/identity.ts — the identity interface the host must satisfy.
  4. siso/database-adapter.ts — the config seam; note it does no data migration.
  5. backend/.../auth/siso-request-context.ts + its .spec.ts — the signed-header middleware and its negative tests.
  6. siso/verification/one-shot-receipt.json — exactly what was proven and what wasn't.
  7. upstream-manifest.json — the pinned donor commits, fetch-only remotes.

06Two things to flag before he starts

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.

07What Camron copies

HALO is Convex-backed and mid-migration off Supabase, so the host side differs. The transferable part is the pattern, not the plumbing.