← All specs
Phase 2 draft — proposed-pending Madara

Social Kernel — the configurable socialization substrate

Four-plane architecture (pseudonym / policy / object / discovery) that makes every future social surface a config entry, not a migration. Phase-2 custodial, built behind the shielded-core interface.

Social Kernel — the configurable socialization substrate

Why this doc exists. Madara (2026-06-10): “build stokasoftware’s socialization infrastructure now and choose an architecture that will allow us to be endlessly configurable given the scope of the canon.” The canon’s scope is large — the Discovery engine is “the anonymous-communication / social layer (Stoka’s answer to traditional social media)” whose algorithm “optimizes for developing the user’s interests further, not retention” (CANON.md §III), with users “always anonymous yet reputation-bearing” (§VII). This spec chooses the architecture. It restates ratified canon (wiki/canon/infra-requirements.md, specs/messaging.md) and adds the kernel design; the kernel design itself is proposed-pending until ratified.

0. What already exists (audit 2026-06-10)

Live in prod:

PieceWhereState
Actor identity (act_<hex32>, handle, x25519 keys)identity.account_actors, identity.actor_keyslive, but one-active-actor-per-account (uq_account_actors_one_active)
DM substrate (E2E-capable envelopes, cipher epochs, per-envelope TTL)msg.threads/envelopes/participants/group_keyslive; SSE realtime + presence + typing in admin/main.py
Social graphsocial.follows, social.blocks + actor-addressed APIslive (“phase 2” block, admin/main.py:4773)
Profiles, recommendations, messages UIsrc/pages/profile/, recommendations/, messages.astrolive
Audience-tier precedentzetsu share substrate: public /wz/, auth /zetsu/, token /trace/live 2026-06-10
Rank lore-RBAC (admin plane)layer2/deidara/roles.yaml — viewer rank R sees X iff R ≤ X.ranklive

Specced, not built: multi-pseudonym (I-R1), per-pseudonym config (bio/topics/ inbox_mode/discoverable/searchable), discovery directory, public content objects (posts), the interest-development feed, reputation.

Canon constraints inherited (ratified, do not re-litigate):

  • I-R1 one human → many simultaneous pseudonyms, cheap mint/burn, no cap.
  • I-R2 cross-pseudonym unlinkability is the END-STATE hard requirement; phase 2 ships custodial (B) behind the C/E interface — the stoka_id FK may exist now but must be quarantined: no public API, payload, or page ever speaks stoka_id; everything public is actor-addressed. The FK lives in ONE module so the shielded migration is one seam.
  • I-R6 / ratified decision 4: abuse handling is per-context only, no global ban, no cross-context correlation capability is ever built.
  • Reputation: per-pseudonym, non-transferable, new pseudonym starts at zero (working default, pending confirm).
  • Discovery feed content = opt-in published provenance (infra-requirements §5) — the feed and provenance are the same data at two visibility levels.

1. The architecture — four planes, all config-driven

The kernel principle is the canon principle: LORE IS CONFIG. A new social surface (a content type, a visibility rule, a feed lens) must be a config entry + renderer, never a schema migration or a hand-rolled policy check.

┌─────────────────────────────────────────────────────────────┐
│  4. DISCOVERY PLANE   directory · topics · feed lenses      │
│     (ranks BY reputation later; interest-development algo)  │
├─────────────────────────────────────────────────────────────┤
│  3. OBJECT PLANE      social.objects + social.activities    │
│     (typed payloads; type registry is config)               │
├─────────────────────────────────────────────────────────────┤
│  2. POLICY PLANE      one audience resolver                 │
│     (public/followers/allowlist/auth/rank≤N/token — config) │
├─────────────────────────────────────────────────────────────┤
│  1. PSEUDONYM PLANE   identity.account_actors, multi-actor  │
│     (acting-as switching; stoka_id FK quarantined)          │
└─────────────────────────────────────────────────────────────┘

Plane 1 — Pseudonym (evolve account_actors, don’t replace)

identity.account_actors is already the right primitive (better than messaging.md’s draft identity.pseudonyms — it has the actor_id shape and the key registry). Evolve:

  • Drop uq_account_actors_one_active → many simultaneous active actors per account (I-R1). Keep uq_account_actors_one_primary (the default acting-as).
  • Add per-actor config (from messaging.md, ratified shape): bio text, topics jsonb default '[]', inbox_mode text default 'closed' (closed|allowlist|open|public), discoverable bool default false, searchable bool default false, retired_at timestamptz (burn = retire, handle released to a tombstone, no forwarding pointer). All discovery toggles default OFF — trust-inverted discovery is opt-in per pseudonym.
  • Acting-as: every social/msg API accepts X-Acting-As: act_…; the backend verifies the actor belongs to the caller’s stoka_id and is active. Omitted → primary actor. (JWT stays account-scoped; acting-as is a per-request claim. Cheap switch = one header, satisfies the messaging.md “one keypress” bar.)
  • Quarantine module: admin/routers/social_kernel.py owns every query that joins stoka_id ↔ actor_id. Nothing else may write that join. This is the C/E seam.
  • Mint/burn endpoints: POST /api/actors (auto-handle ghost-XXXX, rate-limited), POST /api/actors/{id}/retire. No cap, no cooldown.

Plane 2 — Policy (one audience resolver, generalizing the zetsu tiers)

Every object carries an audience jsonb descriptor. ONE resolver evaluates it:

{"tier": "public"}                          // anyone, no auth (like /wz/)
{"tier": "auth"}                            // any signed-in stoka user (like /zetsu/)
{"tier": "followers"}                       // followers of author actor
{"tier": "allowlist", "actors": ["act_…"]}  // explicit share targets
{"tier": "rank", "max": 2}                  // lore-RBAC rule, admin-plane objects
{"tier": "token", "hash": "…"}              // capability link (like /trace/)

resolve_audience(viewer_ctx, audience) -> bool is the only gate, used by objects, feeds, the directory, and (eventually) artifact grants. Blocks are evaluated BEFORE audience (either-direction block = invisible, matching the live follows/blocks semantics). New visibility rules = new tier evaluator registered in config — the rank tier makes the admin plane and the public plane share one mechanism.

Plane 3 — Object (the endlessly-configurable core)

Two tables, ActivityStreams-shaped but local-first:

CREATE TABLE social.objects (
  id           TEXT PRIMARY KEY,            -- obj_<hex32>
  type         TEXT NOT NULL,               -- registry-checked, NOT enum-constrained
  author_actor TEXT REFERENCES identity.account_actors(actor_id) ON DELETE SET NULL,
  audience     JSONB NOT NULL DEFAULT '{"tier":"public"}',
  payload      JSONB NOT NULL DEFAULT '{}', -- type-specific, schema in the registry
  in_reply_to  TEXT REFERENCES social.objects(id) ON DELETE CASCADE,
  refs         JSONB NOT NULL DEFAULT '{}', -- artifact ids, zetsu slugs, urls…
  topics       JSONB NOT NULL DEFAULT '[]',
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
  edited_at    TIMESTAMPTZ,
  deleted_at   TIMESTAMPTZ
);

CREATE TABLE social.activities (
  id           BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  actor_id     TEXT NOT NULL REFERENCES identity.account_actors(actor_id) ON DELETE CASCADE,
  verb         TEXT NOT NULL,               -- like | boost | bookmark | flag | …
  object_id    TEXT NOT NULL REFERENCES social.objects(id) ON DELETE CASCADE,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (actor_id, verb, object_id)
);

The type registry is a config file (layer2/social/object-types.yaml): each type declares its payload schema, renderer, who may author it, default audience, whether it appears in feeds. Launch types: post (markdown ≤ N chars), share (refs a zetsu slug / artifact / url with a comment), reply. Future types (poll, badge, provenance-card, bounty, listing…) are registry entries + an Astro renderer component — no migration. This is the configurability contract.

Replies are objects with in_reply_to; likes/boosts/bookmarks are activities — so “comments” and “reactions” on ANY future surface are already built.

Plane 4 — Discovery (directory + topics + feed lenses)

  • Directory: GET /api/discover/actors?topic=… over discoverable=true actors. Opt-in only. Handle search over searchable=true.

  • Topics: free tags now (jsonb on actors + objects), curated taxonomy later via the registry. Topic pages = filtered object queries.

  • Feed = a stack of configured lenses, not a ranking model. GET /api/feed composes, in declared proportions (config, not code):

    1. following — objects from followed actors (chronological);
    2. topics — objects matching the acting pseudonym’s declared topics;
    3. adjacent — the canon-critical lens: objects from topics near yours that you do NOT follow — the “develop the user’s interests further” injection;
    4. fresh — new discoverable actors’ first posts (cold-start fairness).

    No engagement-optimization, no retention loop — the lens stack and proportions are config (layer2/social/feed-lenses.yaml), so tuning the algorithm is editing a yaml, and the algorithm is inspectable — which is itself an ethos differentiator Stoka can publish. Reputation-weighted ranking slots in later as a lens parameter when the provenance/reputation core lands (it reads, it doesn’t restructure).

2. What this is NOT (yet)

  • Not federation. ActivityStreams shape keeps the door open; ActivityPub federation is a non-goal until the shielded core exists (federating custodial pseudonyms would export the linkage risk).
  • Not the reputation system. That is gated on the shielded provenance core (infra-requirements §5–6). The kernel leaves it a clean read-side join.
  • Not E2E for public objects. Public/auth objects are plaintext by nature; the E2E machinery stays in msg.* where it already lives.

3. Phasing

PhaseShipsSurface
A (this sprint)multi-pseudonym + per-actor config + acting-as + mint/burn/switcher · audience resolver · objects/activities + registry (post/share/reply) · directory + topic + profile-feed pages · feed v1 (following+topics lenses)/discover, actor profile pages, pseudonym switcher in nav
Bshare deep-integration: zetsu/artifact rich cards in DMs and posts (messaging.md’s real mission); telegram doorbellmessages + post composer
Cadjacent+fresh lenses tuned; reputation lens once provenance core landsfeed

Backend (migrations + social_kernel.py router) → codex-xhigh. FE (switcher, discover, feed, composer) → Bernays. Playwright smoke before handoff. BE+FE rebuilt together.

4. Decisions for Madara

  1. Lift one-active-actor now? Recommended YES — I-R1 is the identity primitive and retrofitting acting-as under live content is far costlier than under today’s 11 actors.
  2. Phase A scope: full kernel (objects + feed + directory) vs identity-first (multi-pseudonym + directory only, objects next).
  3. Public surface naming/placement: /discover as the canonical front-door path (canon calls it the Discovery engine) — confirm the name.