Using it from another app
Nothing in the AussieAuth repo changes. The new app registers itself.
This page is the reference behind both ways in — the mechanics of registration, origins and method allow-lists are identical whether the session comes from aussieauth.com (lazy) or from a fork of your own (self-hosted). If you just want it working, read one of those instead; they're three commands each.
One command
bun add @aussieljk/auth
bunx aussieauth init # hosted — aussieauth.com mints the session
bunx aussieauth init --self-hosted # your own deployment mints it
init works out what kind of app it's in — Vite, Next, TanStack Start, Expo —
and writes the provider, a sign-in route and convex/auth.config.ts for it.
Which deployment that config trusts is the whole of the mode flag: hosted names
aussieauth.com as the issuer, self-hosted reads CONVEX_DEPLOYMENT out of the
project's own .env.local and names that. Either way it registers the dev
origin in the same pass, so localhost works before you've read anything about
origins.
--url https://your-deployment.convex.site names an AussieAuth of your own and
implies --self-hosted, which is what you want when the fork is a separate
deployment from the app.
That last step needs no credentials, because a development origin needs none.
AUSSIEAUTH_SECRET is required to register a public origin and nothing
else:
bunx convex env set AUSSIEAUTH_SECRET "<the value from the AussieAuth deployment>"
export AUSSIEAUTH_SECRET="<the same value>" # only for public origins
See why development origins need no secret.
The URL nobody can guess
A Convex deployment answers on two hostnames, and only one of them serves auth:
https://your-deployment.convex.cloud— the query/mutation APIhttps://your-deployment.convex.site— the HTTP router, which is where AussieAuth lives
Pointing the client at .convex.cloud produces TypeError: Failed to fetch
with no status and no body, which is indistinguishable from the deployment
being down. init derives the right one rather than asking, and the CLI
corrects a .convex.cloud URL wherever it's given one.
The provider
import { AussieAuthProvider } from "@aussieljk/auth/convex";
import "@aussieljk/auth/styles.css";
export function Providers({ children }) {
return <AussieAuthProvider>{children}</AussieAuthProvider>;
}
It builds the AussieAuth client and the Convex client, wires them together, and
puts the client where the card can find it. Both URLs come from the environment
(VITE_CONVEX_URL / NEXT_PUBLIC_CONVEX_URL, and VITE_AUSSIEAUTH_URL /
NEXT_PUBLIC_AUSSIEAUTH_URL — or derived from the Convex URL when unset). Pass
authUrl, convexUrl, authClient or convexClient when you want control
over any of them.
It lives on @aussieljk/auth/convex rather than the root entry deliberately:
the card talks to the auth server over plain HTTP and imports no Convex, so it
works in an app that has none. @aussieljk/auth/expo is the same provider for
native.
import { AussieAuthSignIn } from "@aussieljk/auth";
<AussieAuthSignIn appName="My App" />;
The app never redirects to AussieAuth — it talks to the deployment from its own origin, so the only consent screen is the provider's.
Registering by hand
init does this for you. The command it runs is:
bunx aussieauth apps register \
--auth-url https://your-deployment.convex.site \
--slug portfolio --name "Portfolio" \
--origin https://portfolio.com --origin http://localhost:5173 \
--methods google,passkey # omit for all fifteen
--secret is needed only because https://portfolio.com is in that list. Drop
it and the command needs no credentials at all.
Or over HTTP, which is what a server-to-server registration on boot looks like:
await fetch(`${AUSSIEAUTH_URL}/apps/register`, {
method: "POST",
headers: {
authorization: `Bearer ${process.env.AUSSIEAUTH_SECRET}`,
"content-type": "application/json",
},
body: JSON.stringify({
slug: "portfolio", // stable id; survives a domain move
name: "Portfolio",
origins: ["https://portfolio.com", "http://localhost:5173"],
methods: ["google", "passkey"],
}),
});
From then on those origins are trusted, they're in the passkey related-origins list, and sessions created from them are stamped with the slug.
Registration is idempotent, so calling it on boot and letting it re-run is the intended usage — that's what makes a wiped table repair itself.
Reading your own registration
GET /apps/me answers from the calling origin alone:
{
"origin": "https://portfolio.com",
"trusted": true,
"registered": true,
"slug": "portfolio",
"name": "Portfolio",
"methods": ["google", "passkey"]
}
The card fetches this on mount and draws only the methods that will work, so a
method your app didn't register is a button that was never there rather than a
403 you find by clicking. Pass respectRegistration={false} to draw a fixed set
regardless.
trusted and registered answer different questions and the difference
matters: an origin can be trusted through TRUSTED_ORIGINS with no app row at
all — that's how a deployment's own sign-in page works. Only trusted: false
means the request will be blocked.
The endpoint is readable from any origin, registered or not, and that's the
point. A request the browser blocks has no response body to explain itself, so
this is the only way a client can tell "the deployment doesn't know me" from
"the deployment isn't there". It's safe to leave unauthenticated: it tells an
origin about itself and nothing else, and the origin list is already public at
/.well-known/webauthn.
From a terminal:
bunx aussieauth apps show --auth-url https://your-deployment.convex.site --origin https://portfolio.com
Errors that name the fix
Every failure the card shows is run through a translation first, so the three that account for most failed integrations come back with a command in them rather than a diagnosis:
| What happens | What you see |
|---|---|
| Origin not registered | The origin, and aussieauth apps register --origin … |
| Method not in the allow-list | The methods you did register, and the command that adds the missing one |
| Provider credentials unset | The exact variables, and the convex env set that sets them |
| Wrong deployment hostname | .convex.cloud named, with the .convex.site URL to use instead |
A blocked request has nothing to inspect, so the card asks /apps/me — which
can't itself be blocked — before deciding which of those it is.
explainAussieAuthError and diagnoseAussieAuthError are exported if you're
rendering your own UI.
Revoking
bunx aussieauth apps unregister --slug portfolio
Prints what it would remove and stops. Add --confirm and it asks you to type
the slug back before doing it. Over HTTP, POST /apps/unregister answers with
a wouldRemove preview unless the body carries confirm: true.
Existing sessions survive; every new sign-in from those origins stops
immediately. The app's row is kept with a revokedAt rather than deleted, so
re-registering restores its previous method list instead of starting from a
blank one.
What origins are for
TRUSTED_ORIGINS still works and is now the bootstrap list — this site and
whatever you're developing against, so a fresh checkout works with an empty apps
table. Registered apps are added on top of it, per request.
An app's origins do double duty: they're the CORS allow-list and the WebAuthn
related-origins list, so registering is also what lets a passkey created on
aussieauth.com be used from that app. The list is served through
aussieauth.com/.well-known/webauthn, which vercel.json already proxies.
Scheme origins are filtered out of that list — a browser can't act on them, and every entry counts against WebAuthn's five-site limit.
Because browsers honour at most five distinct sites and silently ignore the
rest, /apps/register answers with the slot usage:
{
"slug": "portfolio",
"origins": 2,
"passkeyOrigins": {
"limit": 5,
"active": ["https://aussieauth.com", "https://portfolio.com"],
"dropped": []
}
}
The CLI prints a warning naming any origin in dropped, and /admin shows the
budget as five slots with names in them. If your app's origin lands in
dropped, passkeys won't work from it and nothing else will tell you — revoke
an app you no longer use, or consolidate onto fewer sites.
Method allow-lists
The per-app methods list is enforced at /sign-in/social rather than at the
callback, so a blocked provider never gets as far as showing you its consent
screen.
It fails open for origins no app has claimed, which is what keeps a deployment's own sign-in page working with an empty table.
Testing without a deployment
import { AussieAuthSignIn } from "@aussieljk/auth";
import { MockApi, workingDeployment } from "@aussieljk/auth/testing";
<MockApi handlers={workingDeployment}>
<AussieAuthSignIn appName="My App" />
</MockApi>;
MSW handlers for every endpoint the card touches, plus the wrapper that boots
them — so the card renders in your tests, your Storybook, or a sandbox with no
backend at all. appWithMethods(["google", "passkey"]) and
mountHandlers.appUnregistered cover the allow-list and the unregistered-origin
states. msw is an optional peer dependency; nothing else in the package
imports it.
Questions
Do I have to redeploy AussieAuth to add an app? No. Registration is a runtime HTTP call against the registry; the trusted-origin list is resolved per request.
What happens if two apps claim the same origin? The second registration is refused rather than silently reassigned — taking over an origin would mean taking over that app's sign-ins.
How quickly does a new registration take effect? Within five seconds. The registry is cached per isolate for that long; the isolate that handled the registration invalidates its copy immediately.
Can I use this without Convex on the frontend?
Yes. The root entry imports no Convex — that's why the provider lives on
@aussieljk/auth/convex. Use createAussieAuthClient and
<AussieAuthClientProvider> instead. Only the backend is Convex-specific.
Why did the console warn about a contract generation?
The published client infers its types from copies of the server plugins, so it's
correct only while the deployment runs the same generation of them. The
deployment reports its generation on /aussieauth/status and the package
compares it with the one compiled in, warning once in development. Update
whichever side is behind.