Skip to content
Docs Flowy
Security

Read-only, never cloned,
and no token in the database.

Every claim on this page names the mechanism behind it, because your security team will ask. The last section is the one that has no mechanism — what we have not done, in the same type size as what we have.
Repository access
Read-only
Repositories cloned
None, ever
Tokens in the database
None from GitHub
Markdown sanitized
Before storage
Key material stored
SHA-256 only
Credentials

Three kinds, and what happens to each

The rule underneath all of them: we hold the least durable form of a credential that still works, and where we must hold one we say where and under what key.

GitHub

A GitHub App, read-only, on the repositories you pick.

  • Revocable from GitHub at any moment, without asking us.
  • Installation tokens are minted on demand from the App's private key and held in memory for their hour. Nothing token-shaped is ever written to the database.
  • The private key is the only long-lived secret and belongs in a secret store, not beside the data.
  • Connecting a repository asks GitHub as *you*, not as the installation — so nobody can connect a repository they cannot personally read.

GitLab

A project or group access token you paste, encrypted at rest.

  • Encrypted under its own key, deliberately not the session secret: rotating the session secret signs everybody out and is routine, rotating this one means re-encrypting every credential.
  • A token carrying a write scope is refused at connect, by name.
  • A token with no expiry is refused outright.
  • The consequence is stated rather than worked around: because we will not hold a write-capable token, we cannot create GitLab webhooks, and freshness is a poller instead.

API keys

For agents and CI. The organization's, not yours.

  • Only a SHA-256 of the key is stored. The value is shown once, at mint, and there is no recovery path — because a recovery path would mean it was kept somewhere.
  • A key belongs to the organization rather than its author, so a CI job does not break the day that person changes team.
  • One key names exactly one project. There is no organization-wide key to mint any more.
  • Revoking bites on the next request. Nothing caches the lookup.
Rendering

Markdown permits raw HTML, and that is the highest-severity risk in a product like this

A <script> tag in somebody’s README would otherwise execute in our origin, with the reader’s session. Every document is sanitized server-side against an allowlist before it is stored as HTML — not filtered at render time, where one path that forgets is one path that ships an exploit.

The raw markdown stays the source of truth. Rendered HTML is only a cache, keyed by the content's hash and a renderer version, so hardening the renderer invalidates everything at once and nothing has to be migrated.

Your code

The repository is never cloned, and nothing outside your docs is ever fetched

One API call returns the folder structure plus a content hash for every file. There is no git binary, no working copy and nothing written to disk. Only markdown blobs are fetched, and only from inside the docs root — a source file is a name we may see in a path, never a body we retrieve.

Images inside a private repository are served through an authenticated proxy rather than hotlinked, so they load for the people allowed to see them and for nobody else.

Sign in

Session cookies are encrypted, not merely signed

An OAuth session carries the user's GitHub token, so a signed cookie would be a readable one. Cookies are encrypted; passwords are hashed with Argon2 behind a port, never a library whose dependency Python removed.

Revocation is real rather than nominal. Every authenticated request compares an epoch on the account row, which is the honest price of withdrawing a credential that carries its own validity — a decrypted cookie is not an authenticated one. Signing out every device is one action, and disconnecting GitHub invalidates the sessions that carried its token rather than leaving them working until they expire.

There is a session list: a coarse platform label and an address truncated to a /24. No city, no country, and no guess about whether a sign-in was unusual — those need a geolocation database or a model of usual, and inventing either is how a device list starts reporting incidents that never happened.

Access

Nothing is readable by default, and the browser never decides

Joining an organization hands out no documents. A member reads a project through a team grant or a direct one; somebody in no team with no grant reads nothing at all. There is no visibility setting that quietly confers access to everybody.

Access resolves server-side, in the one query every reader route already flows through. A repository you have lost access to answers 404 — identically to one that never existed, so the response cannot be used to learn what exists.

No role comparison happens in TypeScript. Even the per-row 'may this person be removed' answers the Members screen renders are computed on the server, so no copy of the hierarchy reaches the client to drift.

Network

A URL you supply is treated as an attack surface

A self-managed GitLab host is a URL a customer types and the server then fetches, which is an SSRF primitive. The checks are pure predicates: https only, every resolved address checked against private, loopback, link-local and reserved ranges, and redirects refused outright rather than followed and re-checked.

Webhook order is fixed and never rearranged: raw body, then the signature compared in constant time, then parse. Reading a payload before verifying it is parsing something a stranger sent.

The client address is configuration rather than a header. At the default, forwarded headers are ignored entirely; where proxies are counted, the address is taken from the end a caller cannot extend by prepending.

Agents

A machine credential is narrower than the person who minted it, on purpose

A key can carry a minimum review state, and documents below it answer 404 on every door — the reader routes included, because a floor that stops at one surface is a floor with a second front door. It lives on the credential rather than in a tool argument: a filter the agent passes is a filter the agent can be prompted out of passing.

No tool on that surface mutates a repository document, closes a reviewer's note, deletes an artifact, or asks a model a free-text question. Those are absences rather than settings — there is no flag an administrator can grant, which is the point.

Your content

Document text leaves the box only for an organization that said so

This is the one outbound path carrying customer content, and the switch is off by default — defaulting it on would have started every existing organization sending documents to a third party because a deploy happened. Turning it on is written to the audit log.

An empty document makes no call at all: prompting a model not to invent is a request, whereas not calling it is a guarantee. A provider named without its key refuses rather than returning placeholder text, because a fabricated summary stored against real content is served as real until that content changes.

Evidence

An audit row is written inside the same transaction as the change

Never after the commit, never best-effort, never from a background job. If the change rolls back the record goes with it, and if the record cannot be written the change does not land. The actor's name and the subject's label are copied onto the row, because a deleted team still has to read as the team it was.

The trail is filterable and exportable, and kept for the window your plan sets. Nothing can delete a single row from it — a caller able to remove one row is a caller able to remove the interesting one.

Your controls

What you operate yourself

Two of these are plan-gated and the page says which, because finding that out at the point of need is worse than reading it here.

IP allowlist

Business and Enterprise

Refuse every request from an address you did not name. It reaches API keys as well as browsers — a machine credential is checked at the door before the request is dispatched.

Required GitHub sign-in

Enterprise

Withdraw documents from anybody without a GitHub identity, so access follows an account your organization can see.

Session policy

Every plan

Set how long a session lives, and sign every member out at once when you need to.

Audit retention

Every plan

Choose how long the trail is kept. A longer window is the benefit; the purge runs on a schedule rather than on demand.

Honestly

What we have not done

A security page with no gaps on it is a security page nobody believes. These are real and they are current.

SSO and SAML are not built
The card exists in the organization's security settings and it is static markup. It is part of what an Enterprise agreement covers and is not something you can turn on today. We would rather say that here than let you find it after signing.
No SOC 2, no ISO 27001, no penetration test
None of these have been done. If your procurement process requires one, we are not ready for you yet, and that is a real answer rather than a roadmap promise.
DNS rebinding is closed; connection pinning is not
The address checks run at resolve time. A host that answers differently on the second lookup is handled, but we do not pin the connection to the address we validated.
No customer-managed keys on a deployment we run
Encryption keys belong to the deployment. Holding your own means running your own: an install inside your network is an Enterprise term, and on the deployment we operate the keys stay ours.

Found something wrong with any of this? [email protected]. We would rather hear it from you than read it somewhere else.

Give it read access to one repository and take it away in a click.

The smallest possible trial of any of this is a public repository and five minutes.

Start free