Docs
Sign-in & email
Your app’s users can sign in with Google — no Google Cloud project, no key, no console step — and get their sign-up mail under the app’s own name. Both live in the app’s own auth service: the Supabase project behind its managed database.
Agent tools
appAuthStatusappAuthConfigureappAuthEmailStatusappAuthEmailConnectappAuthEmailDisconnect
Sign in with Google
Tofu's broker at oauth.trytofu.ai owns the one Google client and the one permanent callback: it signs the person in with Google and mints a session inside the app's own Supabase project. The app's side is one line, where its Google button is:
// Continue with Google — Tofu brokers the sign-in, no key and no per-app console step.
window.location.href = `https://oauth.trytofu.ai/start?return=${encodeURIComponent(window.location.href)}`;return is the only input. Tofu checks it against its own record of the app's addresses and refuses anything that is not one of them. A client-rendered app (Vite, React) needs nothing more: the session arrives with the redirect, and supabase.auth.getUser() answers.
The person becomes an account in the app's own project, under the address Google verified, with their name and picture in user_metadata (full_name, avatar_url) and tofu_sign_in_provider: google in app_metadata. Signing in later by an emailed link with the same address reaches the same account.
Apps that render on the server
An app whose client does not read a session out of the address (Next.js with @supabase/ssr) appends &flow=pkce, points return at its own callback route, and redeems the token there:
const params = Object.fromEntries(new URL(request.url).searchParams);
const { error } = await supabase.auth.verifyOtp({ type: params.type, token_hash: params.token_hash });
return NextResponse.redirect(new URL(error ? '/login?error=link' : '/dashboard', request.url));What Tofu writes into the app
Separately from that line, Tofu writes its configuration into the app's own auth service in one request: its Google client, the app's address as the site and the allowed return address, email sign-in kept on, and Tofu's sending setup. The Authentication section does it, and so does appAuthConfigure with confirmed: true for that app. It needs paid hosting and the app's managed database, and it is safe to repeat: an app that already carries the configuration is read, not written again.
An app deployed with deploy --database gets its database through the deploy's own step, so sign-in is then one explicit action after it.
| State | What it means |
|---|---|
configured | A read of the app's own auth service returned exactly what Tofu writes. |
not_configured | Read, and not carrying it yet — or there is nowhere to write it (no database, one that is not ready, or no app address). The message says which. |
disabled | This Tofu deployment cannot write sign-in. Nothing was written and no request was made. |
unavailable | The read produced nothing. It says nothing about the app, and is never shown as not configured. |
Sign-in email
The app's auth service sends its own sign-in mail — confirmation, magic link, password reset, invitation, address change and reauthentication — through Tofu's SMTP setup, rendered by the app's own Supabase project, with the app's name as the sender.
Every message carries two ways to finish: a single-use code, shown large, and a link to the app's own origin. Mail scanners and link previews open links before a person does, and a link that spent its token on the first open would leave the sign-up impossible to finish; a code has nothing to open.
| Sender state | Mail comes from |
|---|---|
| No domain connected | noreply@notify.trytofu.app |
| Your domain verified | noreply@send.<your-domain> |
| Records written, not verified yet | noreply@notify.trytofu.app, so mail keeps working meanwhile |
The one route your app owns
The link in each message is /auth/confirm?token_hash=…&type=… on the app's own origin. It does not spend the token when it is opened; the app does, by redeeming it. Tofu cannot write that route for you. In Next.js (App Router):
import { createClient } from '@/lib/supabase/server';
import { NextResponse } from 'next/server';
export async function GET(request) {
const { searchParams, origin } = new URL(request.url);
const token_hash = searchParams.get('token_hash');
const type = searchParams.get('type');
if (token_hash && type) {
const supabase = await createClient();
const { error } = await supabase.auth.verifyOtp({ type, token_hash });
if (!error) return NextResponse.redirect(`${origin}/`);
}
return NextResponse.redirect(`${origin}/login?error=link`);
}In a client-rendered app the same thing is a small route component that calls supabase.auth.verifyOtp({ type, token_hash }) from the query string when it mounts, then redirects. Either way, verify the token in the app, from the query string — never with a background fetch of the link itself. An app built with Tofu's skill in your agent gets this route.
Sending from your domain
Moving the sender onto your domain adds six records, all under send.<your-domain>, a subdomain that did not exist before: nothing at the apex, where your own mail lives, is moved or overwritten, and removing the six puts the domain back exactly as it was.
| Record | Name |
|---|---|
| CNAME (DKIM, three) | <token>._domainkey.send.<domain> |
| MX and TXT (SPF) | bounce.send.<domain> |
| TXT (DMARC) | _dmarc.send.<domain> |
On a domain you approved Tofu for on Cloudflare (Domains), Tofu writes them; anywhere else they are shown for you to publish. appAuthEmailConnect runs one step per call — create the sending identity, write the records, or switch the sender once the email provider says the domain is verified — and appAuthEmailDisconnect takes the domain back off, returning the app to Tofu's address in the same call.
A domain whose records were written but not yet seen by the provider is not verified, and the app keeps sending from Tofu's address until it is.
Limits
What sign-in does not cover yet
Tofu’s name on the consent screen. Google shows the client’s own verified brand, so people sign in to your app under Tofu’s name. Your own name there needs your own Google client, which is not built.
A Tofu-managed database is required. Auth that lives elsewhere — your own Supabase project, Clerk, Auth0 — is not configured, and nothing is written there.
Google and email only. Tofu configures Google and the app’s own email sign-in, and no other provider.
No Google identity is linked. The account is an email account under the address Google verified, so the app cannot call Google’s APIs on the person’s behalf.
Emailed links name the app’s Tofu address. The auth settings Tofu writes carry the app’s managed address; a custom domain is not added to them.
Existing apps are not offered the route. An app deployed without
/auth/confirmkeeps working through the code in each message; nothing yet proposes that file to it.Your own sending domain is the newest part. Moving the sender onto a domain has run end to end on a test project, and not yet for a hosted app.
Sending limits. Up to 100 sign-in emails an hour per app, and at least 60 seconds between two emails to the same address — kept on purpose, so a sign-up form cannot be used to flood a stranger’s inbox.
The ship-it layer for vibe-coded apps. Your agent wrote it — Tofu ships it.
Works in all coding agents
© 2026 Tofu
trytofu.ai