GuidesVisitor sign-in

Visitor sign-in

The people who use a published app are not PhiWebs users. You have a PhiWebs account; they have an account inside your app. The two are separate systems on purpose — the grocer signs in to PhiWebs, the grocer’s customers sign in to the grocer’s app, and neither can see the other’s world.

An app declares whether it needs visitors to sign in at all. An app that allows anonymous visitors serves anyone with the link; an app that requires sign-in refuses everyone else with a 401 that also names the door it expects.

Two places a visitor can sign in

TopologyWho holds the doorUse it when
PlatformPhiWebs, on your app’s behalfYou want sign-in without running an identity system
ExternalYour own OpenID Connect issuerYou already have one and visitors should use it — Team and Enterprise

An app picks one. If the app authenticates at its own issuer and something calls the PhiWebs door anyway, the answer is WRONG_DOOR — with the issuer it should have gone to instead.

With an external issuer, the visitor’s token is verified against your issuer’s keys on every request. Nothing about their password ever reaches us.

Methods the platform door offers

  • Email and password — the visitor registers in your app and is signed in immediately afterwards.
  • Google, GitHub, Microsoft — the app sends the visitor to the provider using its own address as the return target, then exchanges what comes back.

Two conditions, and the refusals are different on purpose:

  • The app must declare the method. Asking for one it never offered returns METHOD_UNAVAILABLE, along with the methods it does offer.
  • The provider must be configured for the deployment. If it is not, the answer is PROVIDER_NOT_CONFIGURED rather than a failure that looks like the visitor’s fault.

These methods are on every plan. There used to be a tier here — email at the bottom, Google and Microsoft above it — and it is gone: the methods the platform operates cost us an OAuth redirect, while email sign-in is the one where we hold the password and send the mail, so charging for the cheap ones and giving away the expensive one priced the platform backwards. A visitor was also the wrong person to refuse; they bought nothing from us and could fix nothing.

What the plan does decide is the other door: pointing your app at your own OIDC/Entra issuer is part of Team and Enterprise — see pricing. Publishing an app that authenticates at an issuer your plan does not include is reported before it ships, not discovered by the first visitor who cannot get in.

The same email is not the same person

A visitor who registers with a password and one who arrives through Google are two different people, even with an identical address. We cannot prove they are the same human, and quietly merging them on a matching email is how account takeover starts.

The endpoints

All paths are relative to your app’s API base, /api/app/{worldId}/{appId}.

MethodPathWhat it does
POST/registerCreate a visitor with email and password, and sign them in
POST/loginSign in with email and password
POST/oauth/loginExchange a provider code (or access token) for a session
POST/logoutEnd the session; safe to call when already signed out
GET/sessionWho is signed in, or null

A session token is presented on later requests either as x-app-session or as Authorization: Bearer ….

Sign-in endpoints are rate limited. A wrong email and a wrong password give the same answer, INVALID_CREDENTIALS — telling them apart would let anyone enumerate your customers.

A bad token is always rejected, even on an app that allows anonymous visitors. An expired session must not quietly become “anonymous, then”.

Records belong to one app

Records written through your app are scoped to that app, not to your whole workspace. Two apps in the same workspace never share a collection, even when their record types have the same name.

Some record types are owner-scoped as well: a visitor reads and writes only the records they created. That is what makes a per-customer portal a per-customer portal instead of a shared table with a filter drawn on top of it.

See also