mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
docs: update multi-tenant spec search obligation to Postgres FTS
The #1285 conformance checklist and formal spec described the search isolation obligation against the Typesense backend that no longer exists. Keep the obligation intact (search query carries community_id; results never cross tenants; refetch is (community_id, event_id)) and restate the mechanism in terms of the Postgres FTS implementation #1321 ships: the events.search_tsv generated tsvector column, GIN index, community_id filter BitmapAnd-ed with the @@ probe, and ChannelScope::ChannelLessOnly in place of the __global__ sentinel. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
co-authored by
Tyler Longwell
parent
2c6b1b925c
commit
84de626963
@@ -47,7 +47,7 @@ Conformance obligations:
|
||||
| Channel-less global events and DMs | Events with `channel_id = NULL` include profiles, DMs, lists, status, long-form, engrams, membership notifications, workflow commands, and repo announcements; global subscriptions use p/kind gates. | `req.community` when no `h` tag is present. | Community-global. "Global" means visible across channels inside one community, never across communities. DMs are per-community. | `events`, `event_mentions`, replaceable/NIP-33 indexes, reactions, thread metadata, feed tables, and direct ID lookup helpers include `community_id`. NIP-33 uniqueness is `(community_id, kind, pubkey, d_tag)`. | `REQ`, `/query`, `/count`, feed, direct `GET /api/events/{id}`, deletes, reactions, and thread lookups filter by community before event id/pubkey/kind matching. | Single-community global subscriptions and DMs still behave as today. | Regression tests for same event id/d-tag/pubkey in two communities and for DM `#p` not cross-delivering. |
|
||||
| Channels and channel membership | `channel_id` (`h` tag) is the only locality boundary; channels, membership, canvas, topic, DMs, NIP-29 discovery are channel-scoped. | `resolve(h)` must equal `req.community`; channel creation writes `community_id = req.community`. | Community-global channel namespace; channel-local for channel content. | `channels`, `channel_members`, canvas/topic/DM participant hashes, NIP-29 group ids, and channel indexes include `community_id`. `channels.community_id` is immutable. | Mixed-community or unknown `h` tags reject generically. Open-channel discovery lists only channels in the community. | Existing channel UUIDs and `h` tags remain valid after default-community backfill. | Migration lint forbids channel re-tenanting except through an explicitly modeled admission path. |
|
||||
| Workflows, runs, approvals, webhooks, schedules | Workflows are channel-scoped or project/channel-global; triggers fire on matching stored events; schedule/webhook/manual triggers create runs; approval tokens are hashed. | Workflow definition's community from `req.community` at create/update; webhook/schedule/manual routes resolve workflow id inside host-derived community. | Community-global workflow namespace; runs/approvals inherit workflow community. | `workflows`, `workflow_runs`, `workflow_approvals` include `community_id`; workflow id/token hash lookups are scoped; trigger event ids are scoped. | Trigger evaluation only sees events in the same community. Webhook URLs include host-derived community; approval token grants cannot act on another community's same hash/id. | Existing workflow APIs and YAML remain unchanged in default community. | Add tests for identical workflow UUID/approval token hash in different communities and schedule execution isolation. |
|
||||
| Search / Typesense | One collection (`events`) indexes `id`, `content`, `kind`, `pubkey`, optional `channel_id`, `created_at`, `tags_flat`; channel-less docs use `__global__`; relay refetches canonical events from Postgres by hit id. | Search query carries `req.community`; indexed documents carry `community_id`. | Community-global search results; operator-global collection infrastructure may be shared. | Typesense schema adds faceted `community_id`; document id is collision-safe (`community_id:event_id`) or deletes/upserts always include a community filter. Postgres refetch uses `(community_id, event_id)`. | Every `filter_by` includes `community_id:=…` plus channel scope. `__global__` means channel-less within the community, not platform global. | One community can use the same collection and produce the same search results. | Reindex gate required; tests for same event id/content in A and B, deletion in A not deleting B. |
|
||||
| Search / FTS | Postgres FTS over the `events.search_tsv` generated `tsvector` column (GIN-indexed); searchable rows expose `id`, `content`, `kind`, `pubkey`, optional `channel_id`, `created_at`, tag terms; channel-less scope is `ChannelScope::ChannelLessOnly`; the relay refetches canonical events from Postgres by hit id. | Search query carries `req.community`; searchable rows carry `community_id`. | Community-global search results; operator-global FTS index infrastructure may be shared. | Every search query filters by `community_id`, BitmapAnd-ed with the GIN `@@` probe; refetch by `(community_id, event_id)`. | Every query carries `community_id` plus channel scope. `ChannelLessOnly` means channel-less within the community, not platform global. | One community produces the same search results as today. | Tests for same event id/content in A and B, deletion in A not deleting B. |
|
||||
| Redis pub/sub, presence, typing, and cache invalidation | Event fan-out uses `buzz:channel:{uuid}`; presence uses `buzz:presence:{pubkey}`; typing uses `buzz:typing:{channel_id}`; cache invalidation uses `buzz:cache-invalidate`. | Pub/sub calls receive `TenantContext` and derive keys from `community_id` plus channel/pubkey. | Pub/sub and presence are community-global; Redis deployment is operator-global shared infrastructure. | Redis keys include community: `buzz:{community}:channel:{uuid}`, `buzz:{community}:presence:{pubkey}`, `buzz:{community}:typing:{channel_id}`, and community-aware cache invalidation payloads/channels. | Cross-node fan-out must not deliver events to subscriptions in another community. Same pubkey can be online/away differently in two communities. Cache drops only affect same-community membership/visibility caches unless explicitly all-community operator maintenance. | Single-community can preserve existing key names only if deployment is isolated; shared multi-tenant Redis must use the prefixed form. | Add tests for same pubkey presence in two communities and same channel UUID collision in two communities. |
|
||||
| Media / Blossom / S3 | Authenticated uploads return content-addressed descriptors; public `GET/HEAD /media/{sha256.ext}` serves blobs; upload audit has `channel_id = None`. | Upload request host provides `req.community`; Blossom/NIP-98 auth URL host must agree. | Blob CAS bytes may be operator-global shared storage; metadata, authorization, quotas, audit, and visibility are community-global. | Media metadata/audit rows include `community_id`; if object keys stay SHA-addressed, any per-community policy lives outside the raw blob key. | Upload/read authorization uses community context. Shared hash bytes are allowed only as dedup/storage optimization; metadata/errors must not reveal another community's private upload. | Existing media URLs keep working for default community. | Decide whether unauthenticated blob `GET` remains intentionally public; if not, reads need host-scoped auth/visibility checks. |
|
||||
| Git hosting / NIP-34 / object storage | Smart HTTP at `/git/{owner}/{repo}` hydrates from S3 object pointers; NIP-34 repo announcements use `d=repo-id`; pointer key is `repos/{owner}/{repo}/pointer`; git push emits kind:30618. | Git HTTP host gives `req.community`; NIP-98 URL and repo announcement community must agree. | Community-global repo namespace and NIP-34 state; pack/manifests CAS objects may be operator-global if pointers are scoped. | Pointer/name keys include community, e.g. `repos/{community}/{owner}/{repo}/pointer`; NIP-34 replaceable coords include `community_id`; any repo-name registry is `(community_id, owner, repo)` or `(community_id, repo)` per product rule. | Clone/push/read policy resolves repo and branch protections only inside the host community. Git hook policy callback carries community and rejects mismatches. | Existing clone URLs and repo ids work under the default community; object-store migration can move pointers under default prefix without changing git clients. | Add tests for same owner/repo in two communities and push in A not advancing B pointer. |
|
||||
|
||||
@@ -950,13 +950,14 @@ The model's obligations map to concrete code seams:
|
||||
shared atomic seen-set keyed by community and NIP-98 event id, or an explicitly
|
||||
admitted sticky/single-replica deployment; otherwise S2's freshness premise is
|
||||
not carried in production.
|
||||
- **Search / C2.1** — Typesense is shared infrastructure, not a shared result
|
||||
space. Indexed documents carry `community_id`; document ids are collision-safe
|
||||
across communities (for example `community_id:event_id`) or every upsert/delete
|
||||
path includes a community filter. All query `filter_by` strings include
|
||||
`community_id`, and Postgres refetch by hit id is `(community_id, event_id)`.
|
||||
The existing `__global__` channel sentinel means channel-less within one
|
||||
community, never operator-global.
|
||||
- **Search / C2.1** — the Postgres FTS index (the `events.search_tsv` generated
|
||||
column, backed by a GIN index) is shared infrastructure, not a shared result
|
||||
space. Searchable rows carry `community_id`, and every search query filters by
|
||||
`community_id` so the FTS predicate is BitmapAnd-ed with the community-leading
|
||||
btree filters; a hit never crosses tenants and refetch by hit id is
|
||||
`(community_id, event_id)`. The channel-less scope (`ChannelScope::ChannelLessOnly`,
|
||||
formerly the `__global__` sentinel) means channel-less within one community,
|
||||
never operator-global.
|
||||
- **Redis / subscription refinement** — Redis pub/sub keys, presence keys, typing
|
||||
keys, cache invalidation channels, and local-echo dedup labels include
|
||||
community context in any shared multi-tenant deployment. The safe shape is
|
||||
|
||||
Reference in New Issue
Block a user