Platform Architecture
tulpa is a platform where every user has a personal AI agent that manages professional relationships. This page explains how the system is designed and why those design choices matter for the people using it.
Per-user isolation
Every tulpa account is a self-contained unit. The bulk of your data, connections, notes, conversation history, relationship health scores, agent threads, audit trails, lives in its own isolated database instance that nothing else can query against. There is no shared SQL table containing every user’s notes.
This isolation is structural for the primary data store. Each user’s database is a separate container; a bug in access control cannot return another user’s notes because there is no shared notes table to read from.
A small number of shared layers exist for product-wide functionality, and each is scoped on purpose:
- A global registry holds handle-to-user mappings, feature flags, pending invite codes, ephemeral phone-verification tokens, mobile-OAuth exchange blobs, and a cache of public profile fields.
- A product-wide search index mirrors embeddings from some private sources (agent memories, relationship notes, connection metadata) so the agent can run vector queries. Reads from this index are always scoped to the calling user.
- An admin summary store holds per-user totals (handle, email, connection count) for the admin console. It is not used for serving end-user product data.
- Object storage holds avatars and resume uploads.
The agent runtime treats the per-user database as the source of truth for private content. Any data that lives in the shared layers above is either non-sensitive by nature (handle, public profile) or a derived/operational mirror gated by scope checks at every read.
The agent system
Each user gets a personal AI agent with its own cryptographic identity. Identities ship with a separate Ed25519 signing keypair and X25519 encryption keypair; Agent Cards publish both key sets. The agent:
- Processes your requests through the chat interface
- Classifies tasks and retrieves relevant context from your data
- Coordinates with other agents via the INK protocol
- Operates within the permission boundaries you configure
Agent actions are gated by a 4-layer permission model (data access, drafting, actions, integrations) and logged to a tamper-evident audit trail. See Permissions and Approvals for details.
Agent-to-agent communication
When agents coordinate, scheduling meetings, making introductions, exchanging receipts, they communicate through signed messages using the INK protocol. Every message carries an Ed25519 signature so both sides can verify authenticity. Messages can optionally be encrypted end-to-end with ECIES.
The protocol is documented at ink.tulpa.network.
The extension platform
Third-party services connect to tulpa through the Extension API. Extensions operate under a delegation model:
- An extension declares its capabilities in a manifest
- A user installs the extension, granting specific permissions and layer access
- The extension receives a scoped delegation token
- All API calls are signed and verified against the granted permissions
Extensions never get full account access. They see only the data layers and capabilities the user explicitly granted. See the Extension API for the full endpoint reference.
Services
| Service | What it does |
|---|---|
| API | Core backend, agent logic, user data, coordination, extension API |
| Handle renderer | Public handle pages at yourhandle.tulpa.network |
| Web dashboard | User dashboard at app.tulpa.network |
| Mobile app | Native iOS and Android client |
Real-time updates
Handle pages and the dashboard receive live updates over WebSocket. Two connection types exist:
- Viewer, unauthenticated, read-only. Sees public data on handle pages.
- Owner, authenticated. Receives the same public-facing initial snapshot as viewers plus owner-only incremental events for things that change in real time (a new pending action, a freshly-resolved approval). Private dashboard data (notes, audit trail, full conversation history) is fetched via the REST/RPC paths, not pushed over the socket.
Updates are push-based: an initial snapshot on connect, then incremental changes as they happen.