Paste your deployment name or its URL and every command on this page fills itself in. Both .convex.cloud and .convex.site work — auth is served from .convex.site, which is the one that’s easy to get wrong.

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 API
  • https://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 happensWhat you see
Origin not registeredThe origin, and aussieauth apps register --origin …
Method not in the allow-listThe methods you did register, and the command that adds the missing one
Provider credentials unsetThe 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.

Read this page as markdown: /docs/embedding.md