Access control
Every app on AppHaven sits behind a managed login gate by default. You decide who can reach it: keep it private to your project, or make it public. There is no login form to build and no user accounts to run for the gate itself.
You set this per app, in the app's Settings, as an access mode.
Access modes
| Mode | Production | Preview branches |
|---|---|---|
| Private (default) | Members only | Members only |
| Public | Anyone | Members only |
| Public, including previews | Anyone | Anyone |
"Production" is the deployment of your app's production branch. Every other branch is a preview.
- Private is the default. Every deployment, production and previews alike, is reachable only by the members of the app's project. This is the right setting for internal tools and anything not meant for the public.
- Public opens your production deployment to anyone, while preview branches stay private to your project. Use it for a public site whose in-progress branches you would rather not expose.
- Public, including previews makes every deployment reachable by anyone.
Who can reach a private app
A private deployment is reachable by the members of the app's project. To give someone access, invite them from the project's Members page. Once they accept, they sign in with AppHaven and can open the app. A member who only needs access (not the ability to manage the project) sees a simple dashboard listing the apps they can reach, and nothing else.
Removing access is immediate: drop someone from the project and they can no longer reach its private apps.
Changing the mode
Access mode is managed by a project's administrators in the app's Settings. A change applies the next time the branch deploys, so redeploy (or push a new commit) for a new mode to take effect.
Public paths
Sometimes one small part of a private app has to stay reachable without a login: a webhook a payment provider calls, a health probe an uptime monitor hits, an API your partners talk to with their own keys. Public paths let you open exactly those URLs and keep the rest of the app behind the gate.
A public path is an exemption. A request whose path matches one of them skips the login gate entirely and goes straight to your app, with no sign-in and no identity header. Everything else on the app still needs a session.
Anyone can call it, from anywhere, with no AppHaven login. Your app is fully responsible for authenticating those requests itself, for example with a webhook signature, an API key, or a shared secret. AppHaven checks nothing on a public path.
Adding one
Public paths live in the app's Settings, on the same Access card as the access mode, and only a project administrator can change them. Like the access mode, a change applies the next time the branch deploys: add or remove a path and then redeploy (or push a commit) for it to take effect. That matters most when you remove one, because the old exemption keeps working until the next deploy.
Your repository can suggest public paths through the
auth.public_paths block in
apphaven.yaml. A suggestion is inert: it appears on the Access card with the
reason given in the manifest, and it does nothing at all until an administrator
adds it. This is deliberate, so that a commit can never widen what is reachable
without a login. See Building with AI agents
for how that plays out when an agent writes the manifest.
The two pattern forms
There are exactly two shapes, and no others:
| Pattern | What it covers |
|---|---|
/healthz | That one path, and nothing else. /healthz/, /healthz/x and /healthzz all still require a login. |
/api/* | /api/ and everything below it, at any depth. A trailing /* is the only wildcard, and it may only appear at the end. |
The root path / is a valid exact rule: it opens the landing page of your app
and nothing else. Every other path, /about included, still requires a login.
To open the whole app instead, use access mode Public.
A prefix really does cover everything under it, including paths you may not have
had in mind. If /api/* is public, then /api/admin/delete-everything is public
too. Choose the narrowest prefix that does the job.
It helps to group everything that should be reachable without a login under one
clearly named prefix, for example by serving those endpoints from
/api/public/… and exempting /api/public/*. Anyone reading your code or your
Access card can then see at a glance what is open, and nothing else ever lands
under the exemption by accident.
You can set up to 10 public paths per app, each up to 128 characters.
A public path covers the path only. Once /api/* is public, every query string
on it is public as well, including /api/orders?_method=DELETE on frameworks
that honour a method override. If a read and a write share a path, exempting
that path exempts both.
How a request is matched
To decide whether a request is exempt, AppHaven looks at the path portion of the URL only, never the query string. Four rules govern the comparison, and each one is worth knowing before you rely on an exemption.
1. The path is tidied up before it is compared. Percent-escapes are decoded,
. and .. segments are resolved, and repeated slashes are collapsed. The
comparison happens on the result, so you cannot climb out of an exempt prefix
into a protected one.
| Request | Compared as | Result with /api/* public |
|---|---|---|
/api/orders | /api/orders | Public |
/api/../admin | /admin | Requires a login |
/api/%2e%2e/admin | /admin | Requires a login |
//api/orders | /api/orders | Public |
2. Matching ignores letter case. /api/* also exempts /API/orders and
/Api/Orders. The gate treats them as the same path. Your app may well answer
those with a 404, since it decides its own routing, but the login gate will not
stop them. Keep this in mind when you pick a prefix: exempting /api/* exempts
every capitalisation of it.
3. A path containing ;, %25 or %00 always requires a login. These
sequences are the ones where your app could read the path differently from the
way the gate read it, so the gate does not treat such a request as exempt. The
request is not blocked or rejected: it simply falls through to the normal login
gate and works for a signed-in user. Ordinary escapes are unaffected, so
/api/a%20b, /api/caf%C3%A9 and /api/items/foo%2Fbar all stay public.
4. Your app receives the original path, byte for byte. The tidying up in
rule 1 is only how the exemption is decided. Nothing is rewritten: a request for
/api/items/foo%2Fbar arrives at your app exactly as it was sent, escapes and
all. Your routing sees what the client typed.
What a public request looks like to your app
A request on a public path reaches your app with no X-Apphaven-Auth header
at all. There is no anonymous token and no empty value; the header is simply
absent. If a client tries to supply one, AppHaven removes it first, so a header
that is present always came from the gate and a header that is missing always
means the request was anonymous. The
Authentication reference
is the full contract.
What your app receives
When a request passes the gate, it reaches your app carrying the signed-in user's verified identity. Your app reads that identity from a single request header and uses it to decide what the user may do. The Authentication reference is the full contract for that header, and Security covers how the platform protects the path in front of your app.