Foldkit docs

Security & data handling

What isolates your data, what each credential is worth if it leaks, and what we do with what you store.

Tenant isolation

Every row we store belongs to exactly one tenant, and the tenant is part of the query rather than a check performed after it.

  • Structural, not remembered. Application code reaches the database through a scoped handle that injects the tenant predicate itself. Using the unscoped handle is a lint error outside the data layer, so the handful of legitimate exceptions — the code that resolves which tenant a request belongs to, and therefore cannot already be scoped to one — are visible in review rather than ambient.
  • A database backstop.Row-level security policies exist on every tenant-scoped table, keyed on a per-connection setting, so a query that bypassed the application layer entirely returns zero rows rather than somebody else’s.
  • Proven by negative tests.A suite asserts that tenant A cannot reach tenant B’s designs, versions, comments, collaborators, assets, fonts, share links or export jobs, on reads and writes alike. It is validated by sabotage: removing a tenant predicate has to make it fail, and it does.
  • Storage is prefixed per tenant — t/<tenant>/… — so an object key from one tenant cannot name an object in another, and deleting a tenant is a prefix operation rather than a crawl.

What each credential is worth

CredentialLifetimeIf it leaks
sk_…Until revokedFull access to your tenant, within its scopes, from anywhere. Rotate immediately.
pk_…Until revokedNothing an attacker could not do by loading your own page. It only works from origins you verified.
es_…Minutes, single-useOne editor session, once, inside its window.
Access token15 minutesRead/write to that one session’s design for the remainder. Cannot be refreshed without your backend.
Export download URL1 hourThat one file. It is a signed URL — treat it as the capability it is.

Secret keys are stored only as a SHA-256 hash. There is no page, no support ticket and no query that recovers one — if you lose it, you rotate. Publishable keys are stored in full, because they are not secrets and a console that cannot show you yours is a console that makes you rotate a working key to read a value you were meant to publish.

Where a token lives

  • The access token is held in the iframe’s memory. Never localStorage, never a cookie — it dies with the frame.
  • The embed authenticates with a bearer token and never a cookie, so there is no CSRF surface to defend rather than one we chose not to use.
  • Session tokens go in an iframe URL’s fragment, which is not sent to any server, not logged, and not carried in a Referer.

In the browser

  • Framing is server-decided. The editor document carries a Content-Security-Policy: frame-ancestors built from the origins you verified with us. A page that is not on your list cannot frame the editor, whatever it claims about itself.
  • Origins are verified, not asserted. A publishable key is checked against the browser-supplied Origin, which a page cannot forge, and against a list you proved control of.
  • Messages are checked both ways. Every postMessage between your page and the iframe is checked against the window reference first and the origin second — a reference comparison cannot be forged, and an origin string means nothing until you know which window sent it.
  • CORS is per tenant. Cross-origin access is granted to your verified origins and nobody else, and never with credentials — which is what keeps our session cookie out of your page entirely.

Your data

WhatWhere it livesRetention
Designs and their documentsPostgres, tenant-scopedUntil you delete them.
Version historyPostgresPruned on a policy you configure; a named version is never pruned.
Uploaded images and fontsObject storage, tenant-prefixedUntil the design referencing them is deleted.
Export artefactsObject storage, tenant-prefixedRe-renderable from the design at any time.
Diagnostics and delivery logsPostgresA rolling window — “recent”, not “all time”.
Usage recordsPostgresRetained for billing. Independent of any cache.

Your customers are not our users

An end-user of your embed is identified by externalUserId — your id for them, opaque to us. We do not create an account for them, we do not ask them for an email address, and we have no way to contact them. Whatever that string means is entirely yours.

Export and deletion

  • Everything in your tenant is readable through the API you already hold: designs, documents, versions, assets and exports.
  • Deleting a tenant deletes its database rows, its storage prefix, its cached state and its queued jobs. Ask us and we will confirm when it is done.
  • Deleting a design deletes its versions, comments and export jobs with it.

Things we would rather you knew

A security page that only lists strengths is one you stop trusting at the first omission you find yourself.

  • An export download URL is a capability. Whoever holds it can fetch that file for its lifetime, with no further authentication. It is short-lived and unguessable, and it is still a URL — do not put one in a shared log.
  • The sandbox is shared and public. Its keys are in these docs, anyone can read and write it, and it is wiped without warning. Nothing you would mind a stranger reading belongs in it.
  • Foldkit-Version and the idempotency-key header are declared and not yet honoured. They exist so an SDK can send them from day one. Sending them today changes nothing.
  • Real-time co-editing is first-party only. The collaboration socket does not accept partner origins yet, so an embed gets autosave rather than live multi-user editing.

Reporting something

If you find a vulnerability, tell us before you tell anyone else and we will work it with you. If you are debugging rather than reporting, your console’s diagnostics page records refusals with the origin we actually saw and the same codes on the errors page.