Daochi is a mesh-native sync network for app-owned data. It stores public keys and mirrored app data, but never stores client private keys. New clients should use the X-Daochi-* wire headers. The server still accepts shipped X-Ksync-* and X-Inbe-* compatibility headers.
Daochi uses the client account key for identity and authentication. A client proves control of the account by signing a short-lived challenge with ML-DSA-44, and the server then issues a bearer token for normal sync and social API calls.
The legacy typed sync surface is not end-to-end encrypted against the server. Mirrored app data is stored in SQLite as normal typed rows and JSON payloads so the service can sync, compact, export, derive friend leaderboard stats, and delete account data. This is intentional for compatibility. Operational access to the server database or a valid bearer token can read the typed data those credentials allow.
Protocol clients may also sync encrypted_records: opaque per-account private records identified by collection and id. Daochi stores and versions those blobs for relay, export, deletion, and diagnostics, but does not need to read their contents. Protocol v4 advertises this as the dual-write transition path: upgraded clients can keep sending legacy typed rows for compatibility while also seeding encrypted private records for future mesh-capable clients. Protocol v5 makes encrypted records the primary private-data surface for upgraded clients while legacy typed rows remain available through include_legacy_data for compatibility. Released legacy encrypted collections remain valid in v5 so clients do not need an immediate second backfill migration. Public/social projections such as aliases, friend requests, profile icons, and leaderboard stats remain readable server-side by design.
Clients that encrypt the whole sync payload may post an encrypted envelope to POST /api/v1/sync with JSON fields v, nonce, and ciphertext. Daochi authenticates the bearer token, stores the envelope bytes opaquely, assigns a normal server_version, and returns encrypted envelopes newer than X-Daochi-Since-Version. Existing typed clients keep using the same endpoint and JSON shape as before; the server only takes the envelope path when the request body has the explicit encrypted-envelope shape.
Daochi keeps an app registry so data ownership is app-neutral and can be shared across apps without hard-coding product behavior. Registered apps can send app_id in v5 compatibility mode only when that app's registered compatibility policy allows it. Older shipped Inbe clients without app_id continue to sync during the compatibility window. Protocol v6 requests must include a registered app_id, a signed transaction envelope, and encrypted record collections that belong to that registered app.
Protocol compatibility policy: protocol v1 through v5 are valid through 2027-09-01 for legacy clients covered by app policy. When a future protocol version is deprecated, the immediately previous version must remain valid for at least one additional year, and the current protocol version must always stay valid. App manifests declare their own compatibility windows through compatibility_until and legacy_protocols.
API access is scoped by account, with explicit shared surfaces:
- accepted friends can see the account alias and selected profile/leaderboard stats;
- user-created app grants can share
shared.*,friends.*, orpublic.*encrypted record prefixes with another registered app; - pending friend request participants can see the request metadata;
- public governance processes, proposals, and votes are public by design.
GET /api/v1/sync/challenge?user_id=<sha256-public-key-hex>GET /api/v1/sync/diagnosticsGET /api/v1/sync/wsPOST /api/v1/sync/loginPOST /api/v1/syncPOST /api/v1/account/deleteGET /api/v1/appsPOST /api/v1/appswithX-Daochi-AdminwhenDAOCHI_ADMIN_TOKENis setPOST /api/v1/apps/register-signedGET /api/v1/apps/{app_id}GET /api/v1/apps/{app_id}/collectionsGET /api/v1/nodePOST /api/v1/node/mesh/exportPOST /api/v1/node/mesh/importGET /api/v1/tokens/assetsGET /api/v1/tokens/productsGET /api/v1/tokens/issuerGET /api/v1/tokens/balance?app_id=<optional-app-id>GET /api/v1/tokens/ledger?since=<optional-seq>&app_id=<optional-app-id>POST /api/v1/tokens/spendPOST /api/v1/tokens/purchases/google/verifyPOST /api/v1/tokens/purchases/monero/invoicesGET /api/v1/tokens/purchases/monero/invoices/{id}GET /api/v1/tokens/checkpoints/latestGET /api/v1/tokens/receipts/{receipt_id}POST /api/v1/admin/tokens/manual-creditPOST /api/v1/admin/tokens/checkpointPOST /api/v1/account/aliasGET /api/v1/account/exportGET /api/v1/account/app-grantsPOST /api/v1/account/app-grantsPOST /api/v1/account/app-grants/signedDELETE /api/v1/account/app-grants/{id}GET /api/v1/account/app-records?source_app_id=&target_app_id=&collection_prefix=GET /api/v1/friendsGET /api/v1/friends/requestsPOST /api/v1/friends/requestsPOST /api/v1/friends/requests/{id}/acceptPOST /api/v1/friends/requests/{id}/declineDELETE /api/v1/friends/{user_id_hash}PUT /api/v1/profile/statsGET /api/v1/friends/stats?app=&practice=&metric=GET /api/v1/processesPOST /api/v1/processesGET /api/v1/processes/{id}PATCH /api/v1/processes/{id}POST /api/v1/processes/{id}/proposalsPOST /api/v1/processes/{id}/votesDELETE /api/v1/accountPOST /api/v1/account/delete-with-keyGET /openapi.jsonGET /healthzGET /readyzGET /metricsGET /
GET /api/v1/node includes aggregate usage counts for public node dashboards:
registered users, users active in the last 30 days, registered clients, clients
active in the last 30 days, distinct connected WebSocket users, and current
WebSocket client connections. /metrics publishes the same aggregate user and
client gauges with daochi_* names; legacy ksync_* metric aliases remain for
existing dashboards.
The public API hostname should terminate TLS at a reverse proxy and forward to DAOCHI_ADDR, for example 127.0.0.1:8080.
Set DAOCHI_TOKEN_SECRET_HEX to at least 32 random bytes encoded as hex in production.
DAOCHI_ALLOW_EPHEMERAL_TOKEN_SECRET=1 is only for local development because it invalidates tokens on restart and is not a stable server secret.
Bearer tokens are intentionally cacheable client-side credentials, not the user's durable login state. Clients should silently run the challenge/sign/login flow again when a token expires or receives a 401, as long as the local account key still exists. Login responses include server_time as Unix seconds so clients can compensate for local clock skew when caching token expiry. Older clients may ignore it. Only an explicit user logout, account deletion, or local account reset should remove the account key.
The WebSocket endpoint accepts bearer auth through Authorization: Bearer <token>. Browser clients that cannot set custom WebSocket headers may send Sec-WebSocket-Protocol: daochi-sync-v1, bearer.<token>. Legacy ksync-sync-v1 and inbe-sync-v1 subprotocols remain accepted. Daochi rejects ?token= WebSocket URLs so bearer tokens do not leak through request URLs, browser history, or proxy URL logs.
Encrypted envelope clients should send:
Authorization: Bearer <token>X-Daochi-User: <sha256-public-key-hex>X-Daochi-Client: <client-id>when availableX-Daochi-Since-Version: <last-seen-server-version>when requesting only newer envelopesX-Daochi-Limit: <count>when deliberately paging envelope deltas
X-Ksync-* header names remain accepted as compatibility aliases. Envelope bodies are relayed as JSON encrypted_payloads in the sync response. Daochi does not parse the envelope contents beyond checking that v is 1 or 2 and nonce and ciphertext are non-empty. If a response is paged, encrypted_payloads_truncated is true and encrypted_payloads_next_since_version is the next value to send as X-Daochi-Since-Version.
Clients must sign this exact byte string with ML-DSA-44:
daochi-sync-v1
<HTTP_METHOD>
<HTTP_PATH>
<sha256 hex of exact raw request body bytes>
<challenge nonce hex>
Legacy ksync-sync-v1 and inbe-sync-v1 signed-message contexts remain accepted with their matching legacy signature headers.
The challenge response returns nonce as lowercase hex. The challenge is single-use and expires after 60 seconds by default.
Bearer-authenticated POST /api/v1/sync requests must include Authorization: Bearer <token>, X-Daochi-User: <sha256-public-key-hex>, and Content-Type: application/json. Legacy account headers remain accepted.
Protocol v6 POST /api/v1/sync requests must also include X-Daochi-Tx. The header is either raw JSON or base64url JSON with:
protocol_version: 6tx_idandnonce, both replay-protected per accountaccount_id, matching the bearer accountapp_id, matching the request body app IDapp_key_id, matching an active key in the app manifestmethod,path, andbody_sha256for the exact HTTP requestexpires_at, no more than 15 minutes in the futuresignature, an ML-DSA-44 account signature over the canonical transaction messageapp_signature, an Ed25519 app-key signature over the same canonical transaction message
The canonical transaction message is:
daochi-tx-v1
<protocol_version>
<tx_id>
<account_id>
<app_id>
<app_key_id>
<HTTP_METHOD>
<HTTP_PATH>
<sha256 hex of exact raw request body bytes>
<nonce>
<expires_at unix seconds>
Signed POST /api/v1/account/delete and legacy DELETE /api/v1/account requests must include:
X-Daochi-User: <sha256-public-key-hex>X-Daochi-Signature: <ML-DSA-44 signature>Content-Type: application/json
Signed JSON bodies still include user_id_hash for compatibility. The server accepts public_key and signatures as either base64 or lowercase/uppercase hex. This matches the current C client account storage, which keeps ML-DSA-44 keys as hex strings.
The preferred account deletion endpoint is POST /api/v1/account/delete, using the same challenge/signature scheme as login and sync so the private key never leaves the device. DELETE /api/v1/account remains supported for older clients that already shipped with that wire shape.
The website deletion endpoint POST /api/v1/account/delete-with-key accepts user_id_hash plus the full exported account key text. Current exports start with ksync-account-key-v1, and legacy account key exports are still accepted. Current key exports include public_id, and Daochi rejects a request if that public ID does not match user_id_hash. Daochi signs a fixed deletion proof with that private key, verifies it against the registered public key, deletes the account, and does not store the uploaded key.
From this project directory, build with:
make buildThe Makefile builds a minimal static liboqs from vendor/liboqs with SIG_ml_dsa_44 enabled, then passes the right cgo include/library flags to Go. Use make test for the same setup in tests.
Inspect a production database offline with:
./daochi inspect --db /var/lib/daochi/daochi.db summary
./daochi inspect --db /var/lib/daochi/daochi.db doctor <user_id_hash>inspect doctor prints redacted account status, sync versions, table counts, recent client protocol hints, and recent sync audit metadata. Use --full only when you intentionally need unredacted IDs.
Without Nix, install liboqs headers and library on the host, then:
CGO_ENABLED=1 go build -o daochi .Runtime configuration. DAOCHI_* names are preferred. Existing KSYNC_* names remain accepted as compatibility fallbacks:
DAOCHI_ADDR=127.0.0.1:8080
DAOCHI_BASE_URL=https://api.example.com
DAOCHI_DB=/var/lib/daochi/daochi.db
DAOCHI_ADMIN_TOKEN=<optional app registry write token>
DAOCHI_TOKEN_SECRET_HEX=<stable 64+ hex chars shared by every server instance>
DAOCHI_NODE_REGISTRY_PUBLIC_KEY_HEX=<ed25519 public key hex for signed app approvals>
DAOCHI_NODE_REGISTRY_PUBLIC_KEY_HEX_FILE=/run/secrets/node_registry_public.hex
DAOCHI_KNOWN_NODES=Mirror=https://mirror.example;sync=pull;apps=inbe;collections=inbe.*;data=encrypted_records
DAOCHI_NODE_SYNC_TOKEN=<shared secret for trusted node-to-node mesh sync>
DAOCHI_NODE_SYNC_INTERVAL_SECONDS=60
DAOCHI_NODE_SYNC_BATCH_LIMIT=500
DAOCHI_TOKEN_ISSUER_PUBLIC_KEY_HEX=<ed25519 public key hex>
DAOCHI_TOKEN_ISSUER_PUBLIC_KEY_HEX_FILE=/run/secrets/token_issuer_public.hex
DAOCHI_TOKEN_ISSUER_PRIVATE_KEY_HEX=<ed25519 private key hex, issuer nodes only>
DAOCHI_TOKEN_ISSUER_PRIVATE_KEY_HEX_FILE=/run/secrets/token_issuer_private.hex
DAOCHI_TOKEN_PRODUCTS=tokens_small:5000000:1000000000000
DAOCHI_GOOGLE_PACKAGE_NAMES=com.example.app
DAOCHI_GOOGLE_SERVICE_ACCOUNT_JSON=<google service account json>
DAOCHI_GOOGLE_SERVICE_ACCOUNT_JSON_FILE=/run/secrets/google_play_service_account.json
DAOCHI_GOOGLE_OAUTH_CLIENT_JSON_FILE=/run/secrets/google_play_oauth_client.json
DAOCHI_GOOGLE_OAUTH_REFRESH_TOKEN_FILE=/run/secrets/google_play_refresh_token.txt
DAOCHI_TOKEN_DIRECT_PURCHASES_ENABLED=1
DAOCHI_MONERO_WALLET_RPC_URL=http://127.0.0.1:18083
DAOCHI_MONERO_WALLET_RPC_USER=<wallet rpc user>
DAOCHI_MONERO_WALLET_RPC_PASSWORD=<wallet rpc password>
DAOCHI_CHALLENGE_TTL_SECONDS=60
DAOCHI_TOKEN_TTL_SECONDS=3600
DAOCHI_MAX_BODY_BYTES=1048576
DAOCHI_ENCRYPTED_PAYLOAD_MAX_RETURN=0
DAOCHI_ENCRYPTED_PAYLOAD_MAX_ACCOUNT_BYTES=0
DAOCHI_ENCRYPTED_PAYLOAD_RETENTION_DAYS=0DAOCHI_KNOWN_NODES is a comma-separated public peer-node list. Entries can be https://node.example, Name=https://node.example, or Name|https://node.example; /api/v1/node publishes that list as known_nodes so clients and the nodes page can discover node-to-node connections. A peer entry can add semicolon-separated sync policy fields: sync=<pull|push|bidirectional|none>, apps=inbe+ukuvota, collections=inbe.*+profile.public, and data=encrypted_records+app_registry. These policies control what the node sync worker pulls from trusted peers.
Node-to-node mesh sync is disabled until DAOCHI_NODE_SYNC_TOKEN is set. Trusted peers call POST /api/v1/node/mesh/export and POST /api/v1/node/mesh/import with either Authorization: Bearer <token> or X-Daochi-Node-Token: <token>. DAOCHI_NODE_SYNC_INTERVAL_SECONDS enables the background pull worker for peers whose policy is sync=pull or sync=bidirectional; DAOCHI_NODE_SYNC_BATCH_LIMIT caps each export page. The first implemented replication surface is encrypted app records plus the account public key needed to create the local account row; social/account projections and opaque encrypted envelopes remain client/API owned.
Generate a token secret once and keep it stable across restarts and every deployed instance:
openssl rand -hex 32If DAOCHI_TOKEN_SECRET_HEX is missing, Daochi generates a random in-memory secret at startup. That is only suitable for single-process local development: existing bearer tokens become invalid after restart, and multi-instance deployments will reject tokens issued by another instance.
Encrypted envelope limits are disabled by default to avoid surprising existing clients. Set DAOCHI_ENCRYPTED_PAYLOAD_MAX_RETURN to cap each envelope response, DAOCHI_ENCRYPTED_PAYLOAD_MAX_ACCOUNT_BYTES to reject writes that would exceed an account quota, and DAOCHI_ENCRYPTED_PAYLOAD_RETENTION_DAYS only after clients can tolerate older envelope pruning.
Daochi can issue signed token receipts for registered assets. The server verifies a payment provider or admin credit, writes an append-only ledger event, and signs the receipt with the configured Ed25519 issuer key. Clients must verify the issuer key and accept only issuer and asset IDs they trust.
Self-hosted Daochi servers may use the same ledger shape for local assets later, but they cannot create receipts for an issuer without that issuer's private key. If only the issuer public key is configured, the server can expose and verify receipts but cannot credit or spend tokens.
Token ledger and payment-intent rows are financial audit records. They are scoped by account for balance and receipt lookup, but they are not included in normal account data export and are not deleted by account-data cascade.
Token purchases, spends, invoices, receipts, and spend nonces carry app_id, and spend or purchase flows validate that the app is registered. GET /api/v1/tokens/balance and GET /api/v1/tokens/ledger stay account-wide by default for compatibility; adding ?app_id=<app> returns the app-scoped balance or ledger view for that account and asset.
Signed app manifests can publish token policies with asset_id, permission, status, and optional legacy_unsigned_until. If an app has no token policies, token endpoints keep the legacy registered-app behavior. If policies exist, spends and purchases require the matching policy, and unsigned legacy requests are accepted only until that policy's legacy_unsigned_until timestamp. The registration validator caps that unsigned grace window at 365 days.
Daochi records app-scoped token events, but it does not yet move value between separate account and app allocation pools. Treat app-filtered balances as audit views over the ledger. Explicit account-to-app allocation events are still required before calling per-app token distribution fully complete.
Nodes can accept self-contained app manifests through POST /api/v1/apps/register-signed. The request body contains:
manifest, includingmanifest_version,app_id, display metadata, Ed25519 app keys, collection prefixes, capabilities, and optional token policies;manifest_signature, an Ed25519 signature from one active app key overdaochi-app-manifest-v1\n<canonical manifest json>;approval_signature, an Ed25519 signature from the node registry key overdaochi-app-approval-v1\n<app_id>\n<manifest_hash>\n.
Each node decides which registry approval key it trusts through DAOCHI_NODE_REGISTRY_PUBLIC_KEY_HEX or DAOCHI_NODE_REGISTRY_PUBLIC_KEY_HEX_FILE. That keeps registration cross-platform: CLI, TUI, web, mobile, and desktop apps all submit the same signed JSON manifest, and no platform-specific package name is the root of authority.
Protocol v5 clients should put all new private app data in encrypted_records and use typed sync only as a backward-compatibility mirror. Existing released legacy encrypted collections are grandfathered.
New v5 collection names use a dotted hierarchy:
account.v1.manifestfor the per-account hierarchy manifest;private.<app>.v<version>.<collection>for encrypted private app records;shared.<app>.v<version>.<collection>for user-grantable cross-app records;friends.<app>.v<version>.<collection>for friend-visible app records;public.<app>.v<version>.<collection>for intentionally public encrypted/public-record namespaces.
Future private features should add or extend encrypted collections first. Existing v4 encrypted collections may continue to sync without another migration; new private namespaces should use the v5 hierarchy. Legacy typed schema additions are reserved for compatibility with older clients or for public/server-readable projections.
Example nginx server block:
server {
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Use your normal ACME flow for TLS certificates.
SQLite tables mirror the app data:
server_usersserver_meditation_logsserver_habitsserver_habit_daysserver_sessionsincluding session check-in fields such as mood, energy, stress, notes, and tagsserver_session_roundsserver_friend_requestsserver_friendshipsserver_appsserver_app_collectionsserver_app_capabilitiesserver_app_grantsserver_app_grant_auditserver_encrypted_payloadsserver_sync_auditserver_profile_statstoken_assetstoken_ledgertoken_processed_paymentstoken_spend_noncestoken_payment_intentstoken_checkpoints
Deleting an account removes app sync, social, profile, and governance rows through foreign-key cascade. Token financial audit rows are retained as described above.