Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

For compliance reviewers: Synapse’s identity bundle is a signed, human-owned artefact. The human runs syn identity keygen, signs the bundle on export, and the importer verifies against a workspace-local trusted_keys allow-list. Revocation is in-band: a bundle’s revoked array propagates to every consumer who re-imports. The curriculum surface (syn experience, syn skills, syn cv export) is reproducible without an LLM: same inputs always produce identical bytes. ADR-014, ADR-016, ADR-017 cover the design rationale.


1. What a bundle is

me.synapse.json is a single JSON file the human carries across employers. Shape:

{
  "synapse_protocol": "synapse/v1",
  "kind": "identity-bundle",
  "exported_at": "2026-05-25T18:54:15Z",
  "person": {
    "id": "romulo",
    "title": "Rômulo",
    "frontmatter": { /* full YAML mirror as JSON */ },
    "body": "/* markdown body */"
  },
  "attestations": [
    { "type": "project", "id": "cliente-alfa", "role": "tech-lead",
      "since": "2024-01-01", "title": "Tech lead Cliente Alfa" }
  ],
  "skills": [
    { "name": "rust", "count": 17 },
    { "name": "mcp",  "count": 9 }
  ],
  "revoked": [],
  "signature": "base64-64-bytes-ed25519",
  "public_key": "base64-32-bytes-ed25519"
}

The bundle is tiny (a low double-digit KB even with dozens of attestations) and human-readable. Any text editor can open it. That is the point: a regulator or recruiter who does not have Synapse installed can still read the artefact.

2. Signing & verification

# Once per human, kept on the laptop.
syn identity keygen
# wrote private key to ~/.config/synapse/identity/private.key: public: <base64>

# Export: signs with the configured key.
syn identity export --out me.synapse.json --key ~/.config/synapse/identity/private.key

# Import: verifies against [identity] trusted_keys in config.toml.
syn identity import me.synapse.json
# pessoa "romulo" importada de me.synapse.json

The signed bytes are canonical JSON: keys recursively sorted, the signature field omitted, public_key included. Two implementations must agree on the canonicalisation byte-for-byte; that is why Synapse owns both the producer and the verifier today and the format is byte-stable.

Trust model:

  • If trusted_keys is empty, any embedded public_key is accepted. This is the single-user mode: you sign your own bundle, you import it on another machine, it Just Works.
  • If trusted_keys is non-empty, the bundle’s public_key must appear in the list. This is the cross-org mode: an employer or recruiter pins the keys they trust.
  • An unsigned bundle (v0.3 format) errors on import unless you pass --allow-unverified. The imported person stamps verified: false so downstream consumers can see the trust level at a glance.

ADR-016 covers the full rationale. The crypto layer is ed25519 via ed25519-dalek 2.1; no PKI, no Sigstore: local-first by design.

3. Revocation

A bundle author who realises an attestation is wrong (employer dispute, role title was inflated, security incident) does not need to chase down every consumer. Revocation is in-band:

syn identity revoke me.synapse.json --target project/cliente-alfa --key ~/.config/synapse/identity/private.key
# "project/cliente-alfa" revogado: bundle agora lista 1 revogação(ões)

The bundle’s revoked array now contains project/cliente-alfa. The next consumer to syn identity import me.synapse.json filters attestations whose {type}/{id} (preferred) or bare id appears in revoked before writing to attested_involvement. Re-signing happens automatically when --key is provided; without --key, the old signature is dropped so the bundle does not lie about being verified.

Revocation is additive. The author cannot un-revoke; that is a deliberate property: once you have told a consumer “this attestation is wrong”, it stays wrong.

4. Capturing experience

The bundle’s attestations array comes from the self: true person entity’s attested_involvement frontmatter. The CLI to manage it:

# Add an entry.
syn experience add "Tech lead at Cliente Alfa" \
  --target project/cliente-alfa \
  --role tech-lead --since 2024-01-01

# Review.
syn experience list

# Remove every entry matching project/cliente-alfa.
syn experience rm project/cliente-alfa

Each add rewrites the self person’s markdown atomically. The person entity must exist with self: true in its frontmatter: Synapse refuses to guess which person is the human behind the workspace.

As of v0.6 you normally don’t create it by hand. syn setup captures a global self-profile (name, birthdate, profession, employers) into profile.toml, and syn init seeds the canonical self: true person into every workspace from it: “capture once, seed everywhere” (BR-14, ADR-020). Employers become entity(kind=org) nodes, linked from the self person by an employed-by graph edge (recorded under affiliations and materialised by the seed / syn reindex). The seeded frontmatter looks like:

---
type: person
id: ada-lovelace
title: Ada Lovelace
self: true
birthdate: 1815-12-10        # optional; rendered by the detailed cv export
headline: Mathematician      # → cv headline
affiliations:
  - org: analytical-engine-co
memberships: []
---

If there’s no profile yet (a non-interactive install), set self: true by hand on the entity that represents you, or run syn setup and re-init: cv/identity export point you at syn setup when no self person exists. Override the profile path with SYNAPSE_PROFILE.

syn experience is not capture_fact (BR-12.3). It runs synchronously and writes directly. The pending queue is for LLM-proposed writes; experience entries are claims the human is making about their own past, so they bypass the queue.

5. Skills derivation

syn skills ranks the workspace’s tag frequency:

syn skills --limit 10
#    17  rust
#     9  mcp
#     7  cli
#     5  observability
#     ...

A tag that recurs across many entities is a topic the human spends real time on. The v0.4 algorithm counts every occurrence of a tag across every entity in the workspace; the top N (default 20, capped 200) ships in the bundle’s skills array and renders in the CV.

This is the simplest defensible signal. ADR-017 lists the richer signals reserved for v0.5+ (project depth, decision count, owner-resolved attribution). If the ranking surprises you, the fix is usually to clean up tag noise or rename a tag, not to call ranking broken.

6. CV export

syn cv export --out cv.md
# CV gravado em cv.md

syn cv export --style minimal       # flat skill list, no decisions section
syn --format json cv export         # structured intermediate for custom renderers

The Markdown sections are fixed and ordered:

  1. # {name} + optional _headline_
  2. Born: {birthdate}: detailed style only, when the self person has a birthdate (v0.6, SYN-205)
  3. Contacts line (email · github · linkedin · website)
  4. ## Summary: the person body
  5. ## Skills: top N from syn skills (counts in detailed only)
  6. ## Experience: attested_involvement, newest first
  7. ## Recent decisions (detailed only): last 5 notes with kind: decision, capped with an explicit comment marker

Two runs against the same workspace produce byte-identical output. That is the contract: a regulator or recruiter who reproduces the file gets the same bytes you handed them. An LLM is welcome to polish the result afterwards (copy the Markdown into your model of choice and ask for a rewrite) but the canonical CV is the deterministic one.

PDF export is deferred to v0.5+. Today the workflow is syn cv export | pandoc -o cv.pdf if you need PDF.

7. Worked example: full round-trip

# 1. Generate a key (once).
syn identity keygen

# 2. Mark yourself.
$EDITOR people/romulo.md
# … set `self: true`, fill in email/github/headline, save.

# 3. Record involvement.
syn experience add "Tech lead Cliente Alfa" \
  --target project/cliente-alfa --role tech-lead --since 2024-01-01
syn experience add "Dev Cliente Beta" \
  --target project/cliente-beta --role dev \
  --since 2022-06-01 --until 2023-12-01

# 4. Export the bundle, signed.
syn identity export --out me.synapse.json \
  --key ~/.config/synapse/identity/private.key

# 5. Hand the bundle to someone.
#    They configure their workspace's config.toml:
#      [identity]
#      trusted_keys = ["<your base64 public key>"]
#    Then:
syn identity import me.synapse.json
# → pessoa "romulo" importada; verified=true.

# 6. Render the CV (on any machine that has the workspace + bundle).
syn cv export --out cv.md

8. Troubleshooting

“importando bundle sem assinatura” / --allow-unverified. You exported without --key (or the key file was unreadable). The bundle is still importable but the consumer needs to opt in with --allow-unverified. The imported person carries verified: false. Fix it by re-exporting with a real key.

“assinatura do bundle inválida ou não-confiável”. Either the bundle was tampered with after signing (canonical JSON no longer matches the signature), or the public_key is not in the consumer’s trusted_keys allow-list. Compare the bundle’s public_key against your config; add it explicitly via syn identity import --trusted KEY for one-shot acceptance.

Lost private key. You can no longer sign new exports or new revocations of bundles you already produced. Bundles already signed with the lost key remain verifiable forever. The recovery path is: syn identity keygen to make a new key, re-export your bundle with it, and ask consumers to add the new key to their trusted_keys. There is no way to “transfer” trust from the old key to the new one, that would defeat the point of asymmetric crypto.

No self: true person. syn experience and syn identity export both refuse without exactly one person flagged self: true. Add the flag to the frontmatter of the person entity that represents you. If you have multiple, pick one: federation (BR-12.4) does not span identities, each workspace has exactly one self.

8b. Network embedding providers: the privacy trade-off (SYN-165)

By default the v0.5 semantic layer embeds on device (fastembed + BGE-M3): your notes never leave the laptop, which is the whole point of a local-first second brain (BR-12.1).

Two opt-in network providers exist for users who want a hosted model instead:

[enrichment]
provider = "openai"          # or "voyage"; default is local fastembed

[enrichment.openai]
# Prefer the env var OPENAI_API_KEY over a key in this file: a key on
# disk is a secret that may get committed (BR-12.2).
model = "text-embedding-3-small"

Equivalent block for Voyage under [enrichment.voyage] (VOYAGE_API_KEY, default model voyage-3). Both are requested at 1024 dimensions so the vector fits the existing table.

What you are trading. With a network provider, the full text of every entity is sent to that third party on every write: every note, meeting, decision, and person record. The provider’s data-handling policy, not Synapse, then governs that content. This is the explicit exception to BR-12.1, and Synapse makes it loud:

  • syn init prints a warning when a network provider is inherited from a config layer.
  • Each time a network backend is instantiated (CLI write or MCP server start), it prints a one-time content is sent to <provider> warning to stderr.
  • Synapse stores no API key itself: it reads the env var (or your config) at call time and keeps nothing.

The feature is compiled in only with --features network_backends; a default build cannot send anything anywhere.

9. References

  • ADR-014: original bundle format (v0.3 prototype).
  • ADR-016: mandatory ed25519 + revocation (v0.4).
  • ADR-017: experience array, skill derivation, CV format.
  • AUDITABILITY.md: the broader audit story; the identity layer sits next to it.
  • PROVENANCE.md: x-ai-* stamps on AI-authored entities.