6 September 2026 · 8 min read
One Login. One Device. No Exceptions.



Here's a sentence that should never appear in the same paragraph as “high-stakes exam platform”: two students, one login. And yet, until recently, that sentence described Quero perfectly well. Sign in on your phone at home, sign in on your laptop at a friend's place, sign in on a third device just to check, and Supabase's default auth would happily juggle all three sessions at once. No warning. No conflict. Just a quiet, permissive “sure, why not” that a coaching-batch WhatsApp group would have discovered approximately four minutes after launch.
The kind of bug that doesn't look like a bug
This is the sneaky part. Multi-device login isn't a crash, an error, or a red banner. It's a feature, technically, right up until it's a business problem. Two accounts sharing one login means two students effectively get one subscription. It means a batch of six can rotate one paid seat through the whole group. It means, in the worst case, someone sits a mock exam while a friend feeds them answers from the same account on another screen, live, mid-test. None of that shows up in a bug tracker. It shows up in your retention numbers looking suspiciously healthy for the wrong reasons.
Why this is non-negotiable for something like Quero
Quero runs computer-based tests for NEET and JEE aspirants: real proctoring, real timers, real ranking against real peers. That context changes the calculus on session security completely. A social app tolerating a shared login loses a little ad revenue. An exam-prep platform tolerating it loses the one thing its entire product is selling: a trustworthy signal of what a specific student actually knows.
| Risk if sessions aren't enforced | What it actually costs Quero | Who notices first |
|---|---|---|
| Account sharing across a coaching batch | One subscription doing the work of five or six | Finance, in the MRR-per-active-user math |
| Proxy test-taking mid-exam via a second device | Every rank and analytics number tied to that account becomes worthless | The student, when the real exam doesn't match their mock scores |
| A leaked password sitting on an old, forgotten device | Silent unauthorized access with no forced expiry | Nobody, until it's a support ticket |
| Concurrent logins during a live proctored test | A proctoring signal (tab-switch, device change) that can't be trusted | Every downstream integrity claim the platform makes |
For most apps, “someone shared their password with a friend” is an inconvenience. For an exam platform, it's the one failure mode that undermines the entire premise of the product.
Enter Vaibhavi Patil
This fix has two authors, and the credit split matters, so let's get it right. Vaibhavi Patil (AI/ML engineer and web developer) wrote the SQL trigger that actually enforces single-session login at the database level, the real mechanism, sitting where it can't be bypassed by a client-side check someone forgets to call. I'm Smit, Quero's co-founder, and my part was noticing the gap that was left after her fix landed, and closing it with a Realtime layer so the kicked-out device finds out instantly instead of on its next token refresh. Two people, one bug, both of us up late at night to ship it.
Act One: the trigger that does the actual enforcing
The core idea is almost insultingly simple once you see it: every time a new row lands in `auth.sessions` for a user, delete every other row for that same user. One user, one session, enforced by the database itself rather than by application code that has to remember to check.
create or replace function public.enforce_single_session()
returns trigger
language plpgsql
security definer
set search_path = auth
as $$
begin
delete from auth.sessions
where user_id = new.user_id
and id <> new.id;
return new;
end;
$$;
create trigger trg_single_session
after insert on auth.sessions
for each row execute function enforce_single_session();That's it. That's the whole mechanism. No middleware, no client-side session-counting hack, no “are you sure you want to log in elsewhere?” modal that a determined user just clicks through. The moment device B authenticates, device A's session row is gone from the database. Clean, small, and exactly where enforcement belongs: as close to the data as physically possible, so nothing downstream can accidentally skip it.
The catch nobody notices on day one
Here's the part that makes this genuinely interesting instead of a five-minute fix. Deleting a row from `auth.sessions` doesn't touch the access token already sitting in device A's browser. Supabase issues stateless JWTs, tokens that are verified by their own signature and expiry, not by a live database lookup on every single request. Device A can keep making perfectly valid, perfectly authenticated requests for as long as that token has left to live, blissfully unaware that its session row was deleted the moment it happened.
- The session row is gone, but the JWT doesn't know that yet. It's a bearer token: whoever holds it gets in, full stop, until it expires.
- The client only discovers the row is gone when it tries to refresh, which by default happens roughly once an hour, or whenever the app re-checks the session.
- That gap, up to an hour of a fully authenticated “kicked out” device, is exactly the window a proxy test-taker would live in.
A database trigger that deletes a session row is enforcement on paper. Whether it's enforcement in practice depends entirely on how long the old device's stateless token gets to keep pretending nothing happened.
Act Two: from 'eventually logged out' to 'logged out now'
Closing that gap meant giving the old device a way to find out immediately, without waiting on its own refresh clock. Supabase Realtime ships a database function for exactly this, `realtime.send()`, which lets a Postgres trigger broadcast a message on a named channel the moment it fires. So the trigger got one more line:
perform realtime.send(
jsonb_build_object('new_session_id', new.id::text),
'force-logout',
'session-kick:' || new.user_id::text,
false
);Every device belonging to a user subscribes to its own channel, `session-kick:<user_id>`, the moment it signs in. When a broadcast lands, the client doesn't try to be clever about comparing session IDs, it just attempts a session refresh. For the device that just logged in, that refresh trivially succeeds, a harmless no-op. For the device that just got its session row deleted, the refresh fails, immediately, because the refresh token backing it no longer exists anywhere.
supabase
.channel(`session-kick:${userId}`)
.on("broadcast", { event: "force-logout" }, () => {
supabase.auth.refreshSession().then(({ error }) => {
if (error) {
supabase.auth.signOut().then(() => {
router.push("/sign-in?reason=session-elsewhere");
});
}
});
})
.subscribe();One database trigger enforcing the rule. One Realtime broadcast collapsing an up-to-an-hour blind spot into something that resolves in about the time it takes a network round trip to complete. Two very different skill sets, both doing the exact right amount of work for the layer they own.
What this actually buys the platform
- Every subscription maps to exactly one active human, at exactly one point in time. No more “which of these five accounts is the real one” guesswork on the finance side.
- A live proctored test can now trust that a device change means a device change, not a quiet handoff to someone else answering questions off-screen.
- The kicked-out device gets an honest, immediate explanation, “you were signed out because your account was signed in on another device”, instead of a confusing silent failure days or weeks later.
- None of it depends on a client remembering to check anything. The rule lives in the database, which means it survives every future feature, every new client, every developer who's never read this post.
Frequently Asked Questions
Does this mean account sharing is now impossible?
It makes casual, simultaneous sharing pointless, which is where the vast majority of real-world sharing actually happens. Two people can still take turns using the same login one after another, that's a policy problem, not a session-security one, and no amount of engineering fixes “I gave my friend my password.” What this closes is the case where both people expect to be logged in at once.
Does this slow down logging in?
No. The delete happens inside the same trigger, in the same transaction, as the new session being created, and the broadcast is fire-and-forget from the trigger's point of view. There's no additional round trip on the login path itself; the cost lands entirely on the device being kicked out, not the one logging in.
What if a student's wifi drops right as this fires?
The old device just falls back to the original behavior: it discovers the session is gone on its next refresh attempt instead of instantly. Nothing breaks, nothing double-charges, nothing corrupts. The Realtime broadcast is a speed upgrade layered on top of a mechanism that was already correct without it, worst case, you're back to the exact gap this post describes, not a new one.
The actual takeaway
The database trigger was the fix. The Realtime broadcast was the difference between a fix that's technically correct and one that actually holds up the moment someone tries to exploit the gap between “the rule exists” and “the rule takes effect.” On a platform where a single shared login can quietly undermine every score, every rank, and every ounce of trust a student puts in their own mock-test results, that gap was never something Quero could afford to leave open, even for an hour.