AI app prototype auth limits: the only gate that matters
Every other guide on this topic talks about rate limits on Supabase, Auth0, or the OpenAI API. Those are real, but they are not the limit that kills a prototype. The one that does is the sign-in screen you hit before your first prompt.
What people usually mean by auth limits
If you read the pages that currently cover this topic, the answer is almost always some combination of three things. Supabase email OTPs are capped at a few per hour per address. Auth0 has per-tenant rate limits, hard and soft, tuned to stop credential stuffing. OpenAI and Anthropic have per-key token and request budgets that decay into backoffs once you cross them. Those are operational controls, and they matter when you are running a production system with real traffic.
They do not matter when you are prototyping. When you are prototyping, the limit you care about is the one between curiosity and first prompt. Every other AI app maker puts a signup form there. A first-time visitor spends between fifteen and ninety seconds navigating that form, and then a second fifteen to ninety seconds waiting for the verification email. The cost of that funnel is not abandoned users; it is abandoned iterations. You do not try the tool twice because the second attempt is not free.
mk0r's position is that the auth gate belongs where the money is. Publishing to a custom domain costs something. Switching models to a pricier Claude variant costs something. Starting a sandbox and typing into it does not, as long as the pool is warm. So the gate is drawn around the first group and left out of the second.
Signup form
Zero. The landing page does not render a signup form for anonymous users. Firebase signInAnonymously handles identity in the background.
Email verification
Not required for prototyping. Verification is only requested when you upgrade to a Google-linked account at publish time.
Login attempt caps
Never hit during prototyping, because you are already signed in as an anonymous user. The sign-in popup only opens when requireAuth fires.
Session timeout
Firebase anonymous sessions persist in IndexedDB; mk0r also writes a UUID to localStorage under mk0r_session_key so a lost cookie still maps you back to your Firestore session.
Credential collision
Handled by /api/auth/migrate. A Firestore batch moves app_sessions and projects ownership from the old anonymous uid to the signed-in uid.
The first auth limit: the one that fires on mount
Every other AI app prototype maker resolves this as “show a signup form and wait.” mk0r resolves it with five lines of Firebase in the AuthProvider. If onAuthStateChanged reports no user, the provider quietly calls signInAnonymously and re-enters the callback with a fresh anonymous Firebase User. That user owns a Firestore app_sessions doc keyed by a UUID stored in localStorage, which in turn owns the E2B sandbox handed to you by the VM pool on first prompt.
The consequence of this choice is that the word “limit” never applies on the first iteration. There is no OTP to hit a ceiling on, no password field to brute force, no attempt counter to reset. The only identity artifact you produce is an anonymous Firebase uid, and it already exists by the time you reach for the keyboard.
The anchor fact: requireAuth(action) is the gate
The entire auth boundary in the client is a single callback in AuthContext. It takes the action you wanted to run, checks whether the current Firebase user exists and is not anonymous, and either runs the action immediately or holds it in a ref and opens the sign-in modal. When the modal resolves, the queued action fires once. That is the whole contract.
Concretely, three actions in the product wrap their handler in requireAuth: the publish button, the model selector when you switch to a Claude variant, and a few settings that write to a persistent profile. The prompt textarea does not. Neither does the VM preview, the git history viewer, or the version picker. The split is intentional: only the actions that move money or permanence demand a real identity.
Measure the gap
The collision case no one writes about
Here is the scenario every existing playbook skips. You open mk0r on your laptop anonymously, build a prototype for twenty minutes, and click Publish. The modal opens, you pick your Google account, and Firebase tries linkWithPopup to attach that Google credential to the anonymous user. It fails, because that same Google account signed in yesterday from your phone, and the credential already belongs to a different Firebase uid.
Without a migration path, you lose the prototype. The old anonymous uid keeps owning the Firestore session; the new signed-in uid owns nothing. You would need to export, sign out, sign in, and reimport, assuming the product even supports that.
The client rescues the OAuth credential from the error, signs in with it, then calls the migration endpoint. On the server:
A single Firestore batch moves ownership of everything the old anonymous uid owned onto the new uid. Your VM, your project history, your published custom domain all survive the swap. The request is gated by requireAuthOrError on the server, so you cannot migrate onto someone else's uid: toUid is always the uid from the caller's Firebase ID token.
What happens when your anonymous uid meets an existing Google account
Where most playbooks put the gate vs. where mk0r puts it
Auth limits are a moving decision, not a fixed config. Where you draw the gate is the part that actually matters for a prototyping product.
| Feature | Typical AI app maker | mk0r |
|---|---|---|
| First prompt | Behind a signup form + email verification | Anonymous Firebase session, no form |
| Sign-in modal | On page load, before any value is shown | Only when requireAuth fires on a paid action |
| Session ownership | Tied to email, created at signup | Tied to anonymous uid, migratable on upgrade |
| Credential collision | Typically lost work and re-login | One Firestore batch, session survives |
| Model switching | Often free tier only, hard limit by plan | Gated by requireAuth + billing entitlement |
| Rate limit on AI calls | Per user, enforced at the login layer | Per key, enforced near the expensive call |
The four concrete limits that actually hurt iteration
Pull apart what people actually mean when they say “auth limits” in the context of prototyping. It is four things, each with a clear cost.
The signup form
Cost: 15 to 90 seconds to fill, plus the social cost of deciding whether this product deserves an email. mk0r resolves this by never rendering one for anonymous users.
The verification email
Cost: 15 to 120 seconds of waiting for delivery, plus a tab switch, plus the breakable assumption that the email lands in the inbox and not in spam. mk0r does not send one during prototyping.
The login attempt cap
Cost: a forced password reset if your password manager auto-filled the wrong entry five times. This limit is why Auth0 and Supabase publish dedicated docs about it. mk0r never hits it, because the prototyping flow is not a password-auth flow.
The silent session timeout
Cost: a re-auth mid-iteration, which usually means losing in-memory state. mk0r side-steps this by pairing the Firebase user with a UUID in localStorage under mk0r_session_key; even a blown cookie still recovers your Firestore session.
What this architecture is not
This is not a license to skip auth on sensitive routes. The server side uses requireAuthOrError in auth-server.ts on every endpoint that mutates persistent state, and returns a 401 Response with “Authentication required” when the Firebase ID token is missing or anonymous. Billing routes additionally check an entitlement flag via /api/billing/status and redirect unentitled users to /checkout. An anonymous user cannot publish, cannot switch to a paid model, and cannot hit the billing APIs at all.
What it is, instead, is a choice about where the gate lives. The signup wall and the verification email do not add security during prototyping; they add latency. The real security boundary is at the publish action, at the billing action, and at the Firestore rules that require a verified uid to write anything persistent. Moving the gate there, rather than to the landing page, is what makes “instant” actually instant.
Want to see the anonymous-first flow live?
Book a 20 minute call and we will open mk0r.com together, prototype without signing in, then walk the requireAuth path and the /api/auth/migrate batch in the real codebase.
Frequently asked questions
Why is 'auth limits' a prototyping problem and not an infrastructure problem?
Most writing on auth limits answers an operations question: how do I cap Supabase email OTPs, how do I throttle OpenAI calls per user, how do I configure Auth0 to block credential stuffing. Those are real limits, but they bite you after you ship. For an AI app prototype maker, the limit that actually blocks progress is the one on the very first iteration: the signup wall, the verification email you have to wait for, the second password prompt after Google refused your first one. Every one of those is a 15 to 120 second gap between 'I want to try this' and 'I am typing a prompt.' Prototyping loops die at that latency.
How does mk0r avoid the first-prompt auth gate?
The landing page runs Firebase signInAnonymously on mount through onAuthStateChanged in src/components/auth-provider.tsx. If the page loads and there is no Firebase user, the provider quietly creates an anonymous one. That anonymous uid owns your app_sessions doc in Firestore and pairs with the E2B sandbox handed to you on your first prompt. You never see a login screen, never confirm an email, never hit a verification link. The first token you type is the first interaction you have with the product.
Where is the auth gate actually enforced, then?
It is enforced lazily, at publish time, through a function called requireAuth(action) on AuthContext. The gate logic is four lines: if the user exists and is not anonymous, run the action immediately; otherwise store the action in a ref, open the sign-in modal, and only fire the action after the Google popup resolves. Publishing to a custom domain, switching Claude models, and a few other monetized actions call requireAuth; prompt-send, VM boot, and prototype editing do not. The gate is drawn around the handful of actions that cost money to run, not around the 'type a prompt' path.
What happens when my anonymous uid collides with an existing Google account?
This is the interesting edge case. The default path is linkWithPopup, which merges the anonymous Firebase identity with the Google credential so your anonymous uid just gains an email. But if that Google account has signed in before (from another device, say), Firebase throws auth/credential-already-in-use and refuses to link. The client catches that error, pulls the OAuth credential back out with GoogleAuthProvider.credentialFromError, signs in with signInWithCredential, and POSTs /api/auth/migrate with the old anonymous uid in the body. The server runs migrateSessionOwnership in auth-server.ts, which opens a single Firestore batch, updates every app_sessions doc where userId equals the old uid, updates every projects doc where ownerUid equals the old uid, and commits. Your VM, your prompts, your git history, your published domain all survive the identity swap.
What counts as an 'auth limit' inside a prototyping workflow?
Four concrete things: the signup form gating access, the verification email gating first use, the rate-limited login attempts after a typo or a password manager glitch, and the silent session timeout that forces a re-auth mid-iteration. mk0r removes all four for the prototype phase. There is no signup form. There is no verification email. There are no login attempts because you are already an anonymous Firebase user. Sessions persist via a UUID stored in localStorage under mk0r_session_key, which you can copy into another browser tab with a query param (?key=) to move your prototype without re-authenticating.
Does deferred auth hurt security?
It moves the security boundary to where the cost lives. Anonymous users can create and edit prototypes; they cannot publish to a domain, switch to expensive Claude models without billing entitlement, or access billing endpoints. On the server, requireAuthOrError in auth-server.ts verifies the Firebase ID token on every sensitive route and returns a 401 Response if the token is missing or anonymous. Billing endpoints additionally run a /api/billing/status check and redirect to /checkout if the user has no active trial, so there is no path to bill a non-entitled user. The anonymous path has a strict permission set, and the gate is enforced in one place.
If I sign out, do I lose my prototype?
Your prototype is tied to a userId in Firestore, so signing out means the anonymous session becomes unreachable from a fresh Firebase uid. That is the correct behavior: sign-out is a statement that this device should stop having access. In practice the mk0r_session_key in localStorage also clears on sign-out, so the browser falls back to anonymous sign-in and gets a new UUID. If you want to keep access across sign-out and sign-in, the clean flow is to sign in with Google before you sign out, which triggers the migration path above and rebinds everything to a persistent identity.
What about API rate limits on the AI provider?
Those exist, but they are downstream of the prototype gate. mk0r talks to Claude through either a streaming Haiku path for Quick mode or the ACP bridge inside the E2B sandbox for VM mode. The shared API key is rate-limited at the Anthropic layer, not at the Firebase layer. If you need tighter per-user caps (for example, because you invited a team), the right place to add them is on the /api/chat and /api/vm routes in front of the Anthropic call, not in the anonymous auth path. This matches Anthropic's own guidance: put the per-user cap near the expensive call, not in front of the unpaid prototype step.