Security
BriefGate asks your clients for logos, copy — and passwords. That last one changes what this product is: a service that collects credentials is infrastructure, and infrastructure has to say plainly what it does and does not protect you from.
Everything below describes the system as it is built today. Where a protection does not exist, this page says so rather than leaving it out.
What BriefGate is not
It is not zero-knowledge. A secret value is encrypted with a libsodium
sealed box the moment it arrives, and the private half of that key pair lives in
the server environment, not in the database. A stolen database dump therefore
yields ciphertext alone. But the server that receives the value can read it —
that is how it hands the value to your agent — so an attacker who owns the
running process could read a secret in flight. Any product claiming otherwise
while holding the key is claiming something it cannot deliver.
It holds no certification. There is no ISO 27001, no SOC 2, and no external penetration test. BriefGate is operated by one person. This is stated because a security assessment should rest on what exists.
Secrets
A secret item is the only field type that gets its own handling end to end.
- The value is encrypted on receipt with a libsodium sealed box
(
crypto_box_seal) and is never written to disk in plaintext. - The private key is held in the server environment and is never stored in the database, so a full database dump cannot be decrypted.
- Reading it requires an API key carrying the
secrets:readoradminscope. A key without that scope is told the value exists and nothing more. - It is released once. The retrieval is atomic: the first read marks it revealed, and a later read returns the time of that reveal instead of the value. A five-minute window after the first reveal lets the same key repeat the same read, so a request lost in transit does not cost you the value.
- Every reveal is written to the audit log with the key that did it.
- A secret leaves through exactly three doors, and each of them spends the one
reveal:
get_intake_resultswith the right scope, the Reveal button on one item in the dashboard (POST /v1/intakes/:id/items/:key/reveal, owner session required, audited against the person rather than a key), and a ZIP asked for withinclude_secrets=true. The dashboard deliberately reveals one item at a time on a click, never by rendering a page: merely opening an intake must not burn a value.
What this buys you: a client can send a WordPress password to your agent without it sitting in an inbox, a chat log, or a support ticket.
Credentials and sessions
| Account passwords | argon2id |
| API keys, portal tokens | 256 bits of randomness, stored as a SHA-256 digest; plaintext shown once at creation |
| Two-factor | TOTP, optional, per user |
| Client portal access | magic link (one stable link per intake, token stored hashed, valid for portal_link_ttl_days — default 30 — from the most recent send), exchanged for a session cookie scoped to that one intake |
| Portal session lifetime | 14 days |
| Login and sign-up | 10 attempts per hour per IP address |
API keys are hashed with SHA-256 rather than argon2id on purpose. The key is 256 random bits, so there is no dictionary to slow an attacker down with, and a password hash on every agent request would cost tens of milliseconds for no security gain. Passwords, which people choose, get argon2id.
A password reset invalidates every session and does not issue a new one, so TOTP still stands between an attacker with a reset link and the account.
Signing in an agent without pasting a key
Two flows exist besides creating a key in the dashboard and copying it into config, so an agent can be granted access without a secret ever passing through a terminal or a chat transcript.
Device login (npx @briefgate/mcp login) follows RFC 8628. The CLI
requests a device_code and a short user_code, shows the user code, and
polls /v1/auth/device/poll every few seconds. Both codes are stored hashed,
not in plaintext, and expire after 10 minutes if nobody approves them.
Approving one requires an active dashboard session — the tab that clicks
Allow must already be signed in, so the flow cannot be completed by
someone who only has the code. The API key it hands back is a normal API key
in every respect: same scopes, same revocation, same audit trail, and it
appears on the keys page labeled with the client and hostname that requested
it (e.g. "Claude Code on laptop"), so it can be told apart from keys created
directly in the dashboard.
OAuth 2.1 backs the hosted endpoint (https://mcp.briefgate.dev/mcp).
It is the standard authorization-code flow with PKCE (S256 only) for
public clients — no client secret is ever issued or accepted, matching
what an MCP client running on someone's machine or in a browser actually is.
Dynamic client registration (RFC 7591) is open at /v1/oauth/register, and
metadata is published at
https://api.briefgate.dev/.well-known/oauth-authorization-server. Redirect
URIs must be https:// or a loopback address; anything else is refused.
Access tokens are short-lived (1 hour) API keys and are revocable the same
way any key is; refresh tokens last 30 days and rotate on every use — a
refresh token that gets used after being stolen invalidates the legitimate
one, and the theft becomes visible in the audit log as two uses of the same
token in a short window. Authorization codes, access tokens and refresh
tokens are all stored hashed, never in plaintext.
Both flows end where a dashboard-created key does: something revocable shows up on the keys page, and it only ever lands in the account of someone who was signed into the dashboard at the moment they clicked Allow.
Files
- Served only through signed URLs valid for 24 hours. The URLs are not guessable and not listable.
- Scanned for malware before they are made available. A file whose scan errors or times out is withheld, not served unscanned.
- Every file carries a SHA-256 checksum, computed on receipt and returned with the results, so you can verify that what you downloaded is what the client uploaded.
- Stored in Cloudflare R2 with the EU jurisdiction restriction.
Webhooks
The endpoint you register receives every event on the account, so it is treated as an account-wide decision: only the account owner can add, remove or test one, and creating one issues a signing secret shown once.
- Every delivery is signed:
X-BriefGate-Signature: t=<unix>,v1=<hmac-sha256>over<timestamp>.<body>. Verify it before trusting a payload. - The target address is re-resolved and re-checked immediately before each
request leaves the box, not only at registration. Checking only at
registration is a time-of-check/time-of-use gap: a hostname that resolved to a
public address then can resolve to
169.254.169.254now. - Private, loopback, link-local and unique-local ranges are refused, in both their IPv4 and IPv6-mapped spellings.
- Redirects are not followed, so a redirect cannot walk the request into a private address.
- Each attempt times out after 10 seconds; the response body is drained to a 4 KB cap so a hostile endpoint cannot exhaust memory.
- Six attempts with exponential backoff (1 min, 5 min, 30 min, 2 h, 6 h between attempts), then the delivery is marked failed and can be sent again by hand from the dashboard.
410 Gonefrom your endpoint deactivates it — the documented way to unsubscribe from the receiving end.
The payload never contains a portal link. A portal link is a bearer credential into your client's intake, and a chat channel usually has more readers than the intake does.
Where the data is
| Application and database | netcup GmbH, Nuremberg, Germany |
| Files | Cloudflare R2, EU jurisdiction restriction |
| Transactional email | Resend (US) — EU-U.S. Data Privacy Framework and SCCs 2021/914 |
| SMS, only if you enable it | Twilio (US) — same basis |
| Payments | Stripe (US) — billing data only, never client data |
| Error monitoring | Sentry / Functional Software, Inc. (US) — EU data residency, held in Germany |
The full list, with the transfer basis for each, is in the Data Processing Agreement.
Retention and deletion
- Default: contents removed 150 days after completion. Configurable from 1 to 3650 days, per account or per intake.
retention.mode: "on_delivery"removes contents roughly 24 hours after your agent collects the results — the moment the second copy on our server stops being a service and starts being a liability. Use it for anything with credentials in it.DELETE /v1/intakes/:idremoves the intake, every submitted value, every file in R2 and the whole reminder history, immediately and irreversibly.- Deleting your account marks it immediately and completely; the purge runs after 30 days.
- Backups are encrypted with
ageto a public key before they leave the host, so the machine that writes a backup cannot read it back, and the private half is not kept on that machine. They are held for 14 days, which is the window in which deleted data can still exist in a backup.
Auditing
Sensitive operations are written to an append-only audit log recording the
actor, the action, the IP address and the timestamp: secret reveals, key
creation and revocation, seat changes, logins. The application exposes no path
that deletes from it. You can read your own account's log in the dashboard or
through GET /v1/audit.
Abuse and rate limits
- API requests: 60, 600 or 3000 per hour depending on plan.
- Invitations are capped per account per hour (30 on every plan), and — on
the Free plan — also per account per day (10 invitations per UTC day;
no daily cap on Solo, Studio, or Agency). Manual reminders have their own,
separate caps: 1 per intake per hour and 20 per account per hour, on every
plan — they are not part of the daily invitation cap. Neither the API nor
the dashboard can be used to send mail at someone from our domain. Hitting
an invitation cap while creating an intake leaves it as a draft
(
invitation_sent: false) instead of failing the call; hitting one while sending an existing draft returns a429and the draft stays a draft — either way, the response says so rather than silently not sending. - A sender name or custom e-mail wording may not contain a link (web address, bare domain, or e-mail address), on any plan — the only link in a BriefGate e-mail is the portal button. Custom sender name and e-mail wording are themselves a paid-plan feature (see Billing); the Free plan uses a built-in sender identity instead, so a Free account cannot put an arbitrary brand name in the From line.
- Reminders respect quiet hours and stop on a hard bounce or spam complaint — a bounced address is never chased again.
Error monitoring
Application errors are reported to Sentry (Functional Software, Inc.), with
EU data residency — events go to de.sentry.io and are held in Germany. What
is sent: the error type and stack trace, the request method and path (query
strings stripped), the request id, and the account id when known. What is
never sent: request bodies, cookies, Authorization headers or API keys,
email addresses (scrubbed to [email]), anything a client submitted, or
secret values. In the browser, only unhandled JavaScript errors in the
dashboard and portal, and failed API calls (status code only, no response
body) are reported — there are no breadcrumbs, and the URL's query string is
stripped before anything is sent. Sentry retains events for up to 90 days.
Tracking
Next to none. The marketing site counts page views with Cloudflare Web Analytics, which sets no cookie and stores no identifier in the browser (privacy notice §12). Beyond that: no tag manager, no session recording, no advertising pixel — and nothing at all in the dashboard or in the client portal beyond the error monitoring described above. The only cookies are the ones that make sign-in and the portal session work; they are listed in the Privacy Policy.
Reporting a vulnerability
Email privacy@briefgate.dev with enough detail to reproduce it. You will get a human reply. Please do not run automated scanners against the production service or test against another customer's account or portal — report it and it will be checked properly.