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-localtrusted_keysallow-list. Revocation is in-band: a bundle’srevokedarray 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_keysis empty, any embeddedpublic_keyis accepted. This is the single-user mode: you sign your own bundle, you import it on another machine, it Just Works. - If
trusted_keysis non-empty, the bundle’spublic_keymust 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 stampsverified: falseso 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:
# {name}+ optional_headline_Born: {birthdate}: detailed style only, when the self person has abirthdate(v0.6, SYN-205)- Contacts line (
email · github · linkedin · website) ## Summary: the person body## Skills: top N fromsyn skills(counts indetailedonly)## Experience:attested_involvement, newest first## Recent decisions(detailed only): last 5 notes withkind: 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 initprints 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.