Skip to main content

Backup Service

The Backup Service is a Nethermind-hosted REST service. Tools for Humanity has no production access; the service holds ciphertext and metadata only. Its open source repository is backup-service.

Role and guarantees

The service:
  • Stores the and .
  • Authenticates clients via per-operation challenges signed by the relevant factor.
  • Enforces state transitions on sync via the manifest hash (see Structure & Sync).
  • Maps factor public identifiers back to backup_id so a recovering client can locate its backup from a single Main Factor.
It does not:
  • Hold the ‘s decryption key, any , or any factor private key.
  • Inspect the contents of the .
  • Authenticate users beyond verifying signatures and tokens — there is no concept of a Backup Service user account.

Storage layout

  • S3 blobs and JSON, keyed by backup_id.
  • DynamoDB — A FactorLookup table using a composite primary key combining the scope (MAIN/SYNC) and a factor identifier (credential ID for passkey, iss+sub for OIDC, public key for EC keypair) mapping back to backup_id. This is a lookup utility — the source of truth for which factors authorize a backup is the metadata in S3.
  • Redis — Ephemeral state: used-challenge hashes for replay protection (15 min TTL), hashed OIDC nonces (1 year TTL), short-lived per-backup_id locks during create/sync, and post-recovery sync_factor_tokens used to register a new without re-prompting a Main Factor.
The list of endpoints, request shapes, and the ChallengeContext enum live in backup-service/src/routes; the OpenAPI spec is served at /docs on each environment.

Authorization

Three factor types are recognized: The challenge token is bound to the operation. A Sync challenge cannot be replayed against /v1/delete-factor because the embedded ChallengeContext differs. Challenges are single-use (Redis enforces).

What the service rejects

A handful of named errors are surfaced in client code paths and worth knowing by name:
  • manifest_hash_mismatch — client tried to sync from a stale state (see BF-5).
  • unauthorized_factor — factor identifier is in FactorLookup but no longer in the backup metadata; the device is no longer authorized (see Unauthorized Device).
  • backup_does_not_exist — the metadata is gone (the user deleted their backup); local state should be cleared.
  • backup_account_id_already_exists/v1/create collision; some prior creation attempt registered this backup ID. The client retries with a fresh sync factor authorization on the existing record.
  • factor_already_exists — attempted to add a factor whose public identifier is already enrolled.
  • invalid_challenge_context — challenge token didn’t match the operation it was submitted for.
The full enumeration is in backup-service/src/types/error.rs.

Turnkey

Turnkey is the custodian of s for OIDC factors. It runs key material inside TEE-backed enclaves and exposes a REST API; we picked it because no other provider met the combined requirements of OIDC-aware authentication, non-custodial key handling, and an API-first integration model.
All short-lived keys used with Turnkey are set for a duration of 5 minutes unless otherwise specified.

Turnkey User Setup

The initial release of this feature relied on a slightly different user setup. Some early World App Users will have a slightly different setup (with the Root Quorum being auth_user_main solely) before a migration is introduced.
The setup below is the expected state; see Turnkey Migrations for how accounts created by older clients are reconciled to the latest expected state. Each World App user is a Turnkey sub-organization. Throughout the docs and the code, the terms “Turnkey sub-organization” and “Turnkey account” are used interchangeably. Its users fall into four distinct roles:
  1. Ephemeral. A root_user_genesis user is created by World App’s backend to bootstrap the sub-organization.
    • It is a root user.
    • It is created with a short-lived keypair provided by the client.
    • It is also registered with a fallback 0 key (Curve25519) because Turnkey requires every user to have at least one valid authenticator. After the genesis user is no longer needed, the keypair is discarded — the fallback key remains but no one holds its private half.
    • The client deletes this user immediately after auth_user_main is set up.
  2. auth_user_main. Represents the user’s primary authentication and holds their credentials (passkeys and OIDC providers).
    • Permission policy: explicit ALLOW for all activities (condition = true).
  3. break_glass_user. Represents the — the secp256k1 keypair whose public key is encoded into the Backup Account ID. It exists so the Turnkey sub-organization can be deleted during a Reset. Because the Backup Account Key is deterministic and known ahead of time, this user is named break_glass_user_<last 6 hex chars of the Backup Account Key's public key>. It is not a member of the Root Quorum. The user’s policy permits deletion-only of the sub-organization:
  4. sync_factor_user_<suffix>. Represents operations. Each device gets its own user, holding that device’s long-lived API keypair, and its own policy bound to it. Every such policy permits deletion-only activities, so a Sync Factor cannot grant itself recovery powers.
    While the has permissions to delete credentials (e.g. passkey or OIDC factors), it cannot do so on auth_user_main because Turnkey rejects updates from non-root users for root users even when granted by a policy. This is a current limitation with the Turnkey setup, the user must authenticate with a to remove an OIDC Factor or use the /reset disaster recovery.
    The policy filters on activity.action and activity.resource instead of activity.type because the latter is version-specific. The current expansion covers ACTIVITY_TYPE_DELETE_API_KEYS (CREDENTIAL), ACTIVITY_TYPE_DELETE_AUTHENTICATORS (CREDENTIAL), ACTIVITY_TYPE_DELETE_OAUTH_PROVIDERS (CREDENTIAL), ACTIVITY_TYPE_DELETE_SUB_ORGANIZATION (ORGANIZATION), ACTIVITY_TYPE_DELETE_USERS (USER), ACTIVITY_TYPE_DELETE_PRIVATE_KEYS (PRIVATE_KEY), ACTIVITY_TYPE_DELETE_PRIVATE_KEY_TAGS (PRIVATE_KEY), ACTIVITY_TYPE_DISABLE_PRIVATE_KEY (PRIVATE_KEY).
    USER lets a Sync Factor delete itself on logout and clean up stale sibling Sync Factor users. It cannot delete auth_user_main because Root Quorum validation rejects that.
In addition, Turnkey has the concept of a Root Quorum. When the Root Quorum is met, any action bypasses the Policy Engine. We set the Root Quorum to 1-of-1 with auth_user_main as its sole member.

Turnkey Migrations

A sub-organization is created once, by whichever client version the user onboarded with, and is only writable by the user’s own factors. So when the expected setup above changes, only the client can update the account state. Bedrock’s TurnkeyManager.run_migrations is the mechanism that does this: clients call it after every login and periodically, and it applies only the difference between the account and the expected state. An account already in shape performs zero writes. Reads are stamped by the ; changes need a . When no Main Factor is supplied the user is prompted to authenticate. See BF-10. The migration list, the per-environment audience tables, and the safety guards that make each migration skip rather than guess live in bedrock/src/backup/turnkey.

What Turnkey holds vs what the Backup Service holds

Neither holds the ‘s secret key — that is destroyed at backup creation.

Direct vs proxied activities

Most Turnkey activities (importing the OIDC factor secret at enrollment, exporting it at recovery) are sent directly from the client to Turnkey using a stamp computed in Bedrock. The exception is creating a new sub-organization, which requires a stamp from Tools for Humanity’s parent-organization API key — that single activity goes through TFH’s app-backend.
Last modified on August 10, 2026