Three ways in, one account. Google, an emailed six-digit code, and Sign in with Apple.
Notes never require any of this. Writing, editing, notebooks, the review loop, locking and Recently deleted all work with no account and no network, forever. The app asks for an account at exactly one moment: when you try to put something where other people can read it. A build with no credentials compiled in still launches, still takes notes, and still reads the feed — it just cannot sign anybody in.
| Why it exists | |
|---|---|
| What people on Android will actually use | |
| Email code | The escape hatch for anyone who does not want Google holding the relationship |
| Apple | Required by App Store guideline 4.8 once Google ships. Wired now, shown on iOS only |
Guideline 4.8 says that if you use a third-party login to set up a primary account, you must also offer another service that limits collection to name and email, lets the user keep their email address private, and does not harvest in-app activity for ads. An emailed code fails the middle test — the whole mechanism is that we receive the address — so it does not discharge 4.8 on its own. Sign in with Apple does. This supersedes decision 4 in plan.md, which was correct while magic-link was the only option.
Apple is hidden on Android by canUseApple in src/lib/auth-providers.ts. It works there through
a web redirect, but until an iOS build ships nobody has an Apple identity to come back to. When
iOS lands, that constant is the only thing to change.
No passwords, anywhere. A six-digit code means nothing to breach, no reset flow to build, and no credential to store. A code rather than a magic link because a link is mail app → browser → deep link back into the app, and that handoff fails often enough to lose people.
Four things, in this order. Nothing below is inferable from the repo — it all lives in consoles.
Create a project, then apply the schema:
supabase link --project-ref <ref>
supabase db push # applies supabase/migrations/, including the social tablesTake the project URL and the anon key from Project Settings → API.
In the Google Cloud console, create OAuth client IDs under one project:
- Web application — this is the one the app and Supabase both need. Copy its client ID and secret into Supabase → Authentication → Providers → Google.
- Android — package name
com.zacktiger.dailynote, plus the SHA-1 of the signing certificate. You need two of these eventually: your EAS debug keystore and your Play upload key. Get the fingerprint witheas credentials. This client is never named in the code — Google matches it by package name and fingerprint.
The single most common failure is putting the Android client ID in
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID. It must be the web client ID: that is the audience Supabase validates the token against. The symptom is a successful Google sheet followed by "Google did not return a token".
Enable Sign in with Apple on the App ID, create a Services ID and a signing key, and put the key into Supabase → Authentication → Providers → Apple. Skip this entirely for an Android-only launch; the code path is already there.
Create apps/mobile/.env.local (git-ignored):
EXPO_PUBLIC_SUPABASE_URL=https://<ref>.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=<anon key>
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=<web client id>.apps.googleusercontent.comEXPO_PUBLIC_ variables are compiled into the bundle and are not secret — the anon key is
designed for this and is protected by row-level security, and a public OAuth client ID is public by
definition. Never add a service-role key here.
Then rebuild. Google Sign-In is native code, so Expo Go cannot run it:
npx expo prebuild --clean
npx expo run:android # or: eas build --profile development --platform androidSupabase → Authentication → Settings → automatically link identities with matching, verified emails.
Without it, somebody who signs up with Google as you@gmail.com and later types that same address
into the email form gets a second auth.users row — a different account, with none of their
published work. It looks to them like their profile vanished. This is cheap to enable now and
expensive to reconcile later.
Almost nothing, and deliberately.
profiles.idisauth.users.id. The profile is the account wearing a face; there is no second identity to reconcile and no local id to remap when sync lands.- The handle is never derived from the provider. Google hands over a real name and an email; neither is used to prefill anything. Decision 7 in plan-rollout-2.md allows pseudonymous accounts on purpose, and autofilling a real name would quietly convert this into a real-name network.
- Signing out leaves every note on the device. Notes were never the account's.
- Sync. Signing in gives an account but notes are still local-only; Rollout 1 Phase 4 is what
connects them, and it is what makes
auth.users.idon a note row mean anything. - Account deletion. Guideline 5.1.1(v) requires in-app deletion, and plan-rollout-2 section 5 requires that it really remove published content. Neither exists yet.
- Rate limiting on code sends, beyond whatever Supabase does by default.