Security
How we keep your apps private
Every app on Forculus sits behind our edge. This page says what the edge checks, how apps and workspaces are kept apart, and how to report a problem.
The edge is the only way in
An app deployed on Forculus has no address of its own. JavaScript apps run in a dispatch namespace with no routes, and container apps have no public ingress. The only way to reach either is the edge Worker, which handles every request for every app before the app sees it.
The edge works out who the caller is from their session or bearer token, refuses cross-site writes, and checks the workspace's access rules. Only then does it forward the request. A request that fails any step is denied and logged. Our own components also reach apps only through the edge.
Signed identity
For each request it allows, the edge creates a fresh identity token and passes it to the app in the X-Forculus-Identity header. The token is a JWT signed with the platform's private key, valid for 60 seconds and bound to the app it is for. Before telling your code who is calling, the SDKs check the signature against the platform's published key set, and check the issuer, the audience and the expiry.
Before the edge sets that header it deletes every inbound header that could carry an identity, including any X-Forculus-* header a client sent. A header a client sends can't pass for one the platform set. The edge sets exactly two headers about the caller: X-Forculus-Identity, the signed token, and X-Forculus-Email, a convenience for display that the SDKs never rely on.
Failing closed
If configuration is missing, an access-rule lookup fails or the database returns an error, the request is denied. If an SDK can't reach the key set, it refuses the identity token. Access is denied unless a rule allows it. Every platform Worker checks its own configuration on every request and refuses to serve when a value is missing.
Revocation within a minute
Removing someone from an app takes effect within 60 seconds, including for connections that are already open. The edge watches long-lived responses and closes them when the grant behind them goes away. A revoked bypass token is refused on its next request, and connections already open with it close within 15 seconds. Revoking a browser session, from the account menu, ends it in the dashboard at once and in every app within 60 seconds. Tokens are stored as hashes, and each can be revoked on its own.
Workspaces are kept apart
Every table in the control-plane database that holds workspace data carries a row-level security policy, enforced by Postgres itself, so a query can never return another workspace's rows, whatever the application code does. Each app runs in its own isolate or container with its own storage, its own secrets and its own logs. Every change to the platform runs tests on a full copy of it. They deploy apps for two workspaces side by side, on both runtimes: JavaScript apps, and container apps in real containers. They check that neither workspace's apps can read the other's database, files, secrets or logs.
An app's outgoing traffic goes through an egress proxy that refuses connections to the platform's own hosts and to private addresses, so an app cannot reach the control plane, the edge or another app from the inside. The one exception is the public key set at edge.forculuslabs.tech that apps verify tokens with.
Apps hold no secrets
Third-party credentials your app needs live in the control plane, encrypted with a platform key. The egress proxy attaches them to the app's outgoing requests at the moment of the request. They are never shown back to a person after being set, and never appear in the app's code, its environment listing or its logs. They are not attached at all while the workspace is suspended or once it is deleted. While an app isn't being served, the egress proxy refuses every request it makes. A webhook provider's signing secret, set on a public path, is kept the same way, but the control plane decrypts it to check every delivery sent to its path, and the app gets the platform's signed webhook token instead of the secret. A Postgres database you connect is kept the same way: its password is encrypted in the control plane and given only to Cloudflare Hyperdrive, which holds the connection to it. Apps send their SQL through the egress proxy, which lets through only the databases an app was given, and never see the password. The address must be one the internet can reach: private and internal addresses are refused. Environment variables are not secrets. Their values are stored as you set them, and anyone who can edit the app can read them. Platform secrets are held in the providers' secret stores and never in source code or CI logs.
Every deploy is scanned
Before a version goes live we look for keys and tokens left in what it deploys, and check the packages its lock files pin against OSV (osv.dev), the open vulnerability database. Only package names and versions go to OSV, and never a private package’s. Private keys and live tokens are always refused. For everything else a workspace chooses whether a finding warns or refuses the deploy, and each scan is kept with its version and in the audit log. How scans work.
Encryption
All traffic is TLS. Data at rest is encrypted by the providers that store it, Cloudflare and Neon. Secrets get the extra layer described above. Session cookies are __Host- prefixed, signed, host-only, and verified on every request. Sign-in codes and tokens are stored hashed. A passkey must verify the person, with a fingerprint, face or PIN, and works only on the dashboard's own address.
Audit log
Every request to every app and every administrative action is written to an append-only audit log with who did it, when and from where. The workspace's owners and admins can read it and follow it live, and the owner can export it on every plan. Retention is 3 months on Free and Team and 12 months on Business and Enterprise, after which entries are deleted within two days. After any change of plan, entries older than the new plan's retention are deleted 30 days after the plan changed. Most entries older than 30 days are kept in Cloudflare R2, which Cloudflare is asked to keep in the Asia-Pacific region, encrypted with a key that belongs to the workspace.
Where data is kept
The control plane’s database is in AWS ap-southeast-1 (Singapore). Apps’ databases, files and containers are kept and run by Cloudflare, in the Cloudflare region nearest where the person who created the app was. The archived audit log is in Cloudflare R2, which Cloudflare is asked to keep in the Asia-Pacific region. Requests are served from Cloudflare’s network around the world.
Keeping an Enterprise workspace's data in the EU isn't open yet. Write to sales@forculuslabs.tech to hear when it is. The docs say what it will cover and what it won't.
Backups and recovery
Each kind of data has its own way back, and these are the recovery point (RPO: the newest state a restore can bring back) and the recovery time (RTO: how soon it is back) for each.
- An app’s database is Cloudflare D1, which keeps a point-in-time history of the last 30 days. A workspace owner chooses the recovery point, a minute in that time or, after a restore, the bookmark it gave, and starts the restore from the dashboard or the CLI whenever they need to, and can undo it afterwards.
- The control plane’s database holds workspaces, members, sharing, settings, encrypted secrets and the last 30 days of the audit log. It runs on Neon, which can put it back as it was at any moment in the last 7 days, to the second: that moment is the recovery point. Our recovery time target is 4 hours from deciding to restore. It runs in one AWS region, ap-southeast-1 in Singapore, and we keep no copy of it outside that region yet: while that whole region is down, so are the dashboard and the API, and apps answer 503 within a minute until it is back. Their databases and files are kept by Cloudflare, not there, and lose nothing.
- An app’s files are kept in Cloudflare R2, which Cloudflare designs for 99.999999999% annual durability. R2 keeps no history of a file, so one an app overwrites or deletes cannot be brought back.
- An app’s code is kept as every version deployed, and a rollback puts an earlier one live at once.
- The audit log older than 30 days is kept in R2, encrypted with the workspace’s own key, as above.
- A deleted app or workspace is kept for 30 days, with its data, and whoever could delete it can restore it until then. Nothing is kept while anyone who owns what you delete has yet to accept our terms: see when deletion is immediate. One deleted for good can’t be brought back.
The docs say how to restore each.
How we work
- Every change passes an automated suite before it is deployed, including an adversarial authentication test set: no token, malformed tokens,
alg: none, algorithm confusion, wrong key, wrong issuer or audience, expired and not-yet-valid tokens, replayed tokens, client-supplied identity headers, revoked grants and tokens, direct invocation of an app outside the edge, and key rotation and key-set outages mid-request. A change to authentication adds a case. - Every change is deployed to staging, and its checks pass there, before it is deployed to production, with no exception.
- Staff access to production is limited to what operating the service needs, goes through the providers' own access controls, and is logged.
- The SDKs are open source, under the Apache License 2.0, at github.com/Forculus-Labs/forculus, so you can read exactly what your app trusts. The CLI is not open source: it is released there as binaries only, each with its SHA-256 checksum beside it.
Your part
- Verify the identity token. The SDKs do it in one call. If you don't use them, verify the signature,
iss,audandexpyourself, and never trust a header alone. - Treat bypass tokens as secrets. Create one per script, name it, and revoke it when the script is retired.
- A public path is open to the internet. The edge rate-limits anonymous requests to it per client, and they count toward the workspace's request allowance like any other request. A path can require a provider's signature, which the platform checks before the app sees the request, with a secret that never reaches the app. On a public path that doesn't require one, the app must check a sender's signature itself.
- Keep control of the mailbox you sign in with. It is your account. A passkey saves typing a code, and you can remove one you don't recognize under Sessions.
Reporting a vulnerability
If you find a security problem in the platform, the SDKs, the CLI or this website, email security@forculuslabs.tech. Include what you found, how to reproduce it, and what you think the impact is. We acknowledge reports within two business days, keep you informed while we fix the problem, and credit you when it is fixed if you would like that.
We ask that you:
- Test against your own workspaces and apps, never someone else's.
- Stop as soon as you have enough to show the problem. Don't access, change or delete data that isn't yours.
- Don't run denial-of-service tests, spam, or social engineering against our staff or customers.
- Give us a reasonable time to fix the problem before you publish it.
If you follow those rules we won't take legal action against you for the research, and we consider it authorised under our terms. We don't currently run a paid bounty programme. Problems in a customer's own app belong to that customer. If you can't reach them, tell us and we will pass it on.
Reporting abuse
To report phishing, malware, spam or other abuse on an app hosted on forculuslabs-apps.tech, email abuse@forculuslabs.tech. Include the app's address, what you saw and when. We reply within two business days, and we take down an app that is being used for abuse.
Report child sexual abuse material to the same address. Send the app's address, never the material itself. We take it down and report it to the Singapore Police Force and to the National Center for Missing & Exploited Children (NCMEC).
Send copyright complaints to legal@forculuslabs.tech, the address we have set up to receive takedown notices under Singapore's Copyright Act 2021. The terms say what a notice must include and what we do with it.