VIEWPORT
Concepts

Trust and privacy

What's stored, what isn't, what the relay can see, and the cryptographic model. The auditable version of the marketing pitch.

Viewport's privacy claim is specific. This page is the auditable version of the pitch, with citations into open-source code where it matters.

The four invariants

  1. Session transcripts are never persisted server-side. The daemon owns them. The platform has no table to leak from.
  2. Context Vault entries are HPKE-encrypted on the client. The platform stores ciphertext. Server staff cannot read context.
  3. Relay frames are E2EE between daemon and client. The relay sees envelopes (workspace, runtime target) but cannot decrypt payloads.
  4. Per-org daemon identity prevents cross-tenant correlation. Each org binding has its own keypair + machineId; admins in org A cannot identify a machine as also belonging to org B.

Each invariant is implemented in code in the open-source daemon and relay.

What's stored, what isn't

ObjectStored at rest?Where?
Session frames (prompts, tool calls, transcript)No.Daemon only, under ~/.viewport/.
Plans (the reviewable artifact)YesPlatform DB. They're meant to be shared.
Inbox itemsYesPlatform DB. They're decision queues.
Context Vault entriesCiphertext only.Platform DB. Keys never sent.
Context Vault candidates (proposed)Ciphertext only.Same.
Workflow definitionsYesPlatform DB + mirrored to repo files.
Workflow run recordsYesPlatform DB. The run is auditable; the transcript is not.
Audit eventsYesPlatform DB. That's the point.
Daemon identity keypairLocal only~/.viewport/relay-identities/{workspaceId}.json, mode 0o600.
Per-binding install/runtime/machine idsYes (per org)Platform DB. Different per org.
Relay frame payloadsNo.In-flight only. Lost on relay restart.
Pairing codesYes (until consumed or expired)Short-lived, ULID, expires in 15 min.
API tokensHashedArgon2 hash; raw token shown once on creation.
Pre-shared keys (PSK) for pair-time authYesHashed.

The daemon ↔ relay path

Rendering diagram...
  • Handshake: Noise IK or IKpsk2 (selected per workspace).
  • Session crypto: AES-256-GCM with per-direction nonces.
  • Envelope: workspace id, runtime target id, frame type. Visible to relay for routing.
  • Payload: opaque ciphertext. Relay cannot read.

Conformance vectors for replay-testing the handshake: packages/daemon/docs/relay-noise-v3-conformance-vectors.json. Run npm run test:conformance in the daemon repo to replay them.

What the relay sees

The relay is operationally stateless. Per-process state only:

  • In-memory connection registry (workspace → connections).
  • IP-level rate limit counters.
  • Recent log buffer for /logs admin endpoint.
  • Prometheus metrics counters.

All of this is lost on relay restart. No payloads are persisted. The relay can identify the envelope of every frame. Which workspace, which runtime target. But not the content.

If you self-host the relay, the wire path stays in your network. You control TLS, rotation, and any IP allow-listing. See Self-host: Security posture.

What the control plane sees

The control plane (Laravel API + Postgres + web app) sees:

  • Plaintext: members, teams, machine metadata (name, OS, last seen), workflow definitions, plan artifacts, inbox items, audit events, run records, pairings, settings, integrations.
  • Ciphertext only: context vault entries.
  • Never: session frames (no table), context plaintext (no key material).

In a worst-case scenario where the platform is compromised:

  • The attacker can read members, teams, workflow definitions, plans, inbox items, audit history.
  • The attacker cannot read context vault content. Decryption requires private keys held only on devices granted access via the vault key-share machinery.
  • The attacker cannot read session content. There's no table to read from.
  • The attacker can revoke pairings (effectively a DoS). The attacker cannot impersonate a daemon to a relay without the daemon's private key.

The cross-tenant story

Each tenant (workspace) is isolated through:

  • Row-level scope at the DB layer. Every tenant-owned table carries workspace_id. A global Eloquent scope filters every query by the active workspace. Forgetting a filter no longer leaks because the scope makes unscoped queries return nothing (or throw, in tests).
  • Per-org daemon identity. Different machineId, different keypair, different installId. A machine in org A is cryptographically indistinguishable from any other org A machine when viewed from org A's data, and is invisible from org B.
  • Workspace-scoped teams and share groups. A team or share group from org A cannot grant access to an org B resource. Enforced at write time (ACL entry validation) and read time (the ResourceAccess service double-checks).
  • Audit logs are tenant-scoped. Cross-org queries return nothing for non-Viewport-staff actors.

What's not yet enforced

  • Schema-per-tenant or DB-per-tenant isolation. Single shared DB with row-level scope. Sufficient for SMB and mid-market. Some enterprise compliance contexts require harder isolation.
  • HSM-backed crypto. Daemon keys are file-based with 0o600 perms. HSM/TPM-backed signing is on the roadmap.
  • Customer-managed encryption keys (CMEK). Context vault keys are managed client-side today; bringing your own root key to wrap them is roadmap.
  • Air-gapped operation. The daemon needs to reach the relay. Fully offline operation isn't yet supported.

How to verify the claims yourself

Reporting a vulnerability

We run a coordinated disclosure program. Email security@getviewport.com with details, expect a reply within 48 hours, and 90-day public disclosure unless we agree on a longer timeline. See Security policy.

Where to go next

On this page