Plexivo Auth
Sign users up and in with a password, an email code, Google, GitHub or Microsoft, or as guests, with sessions on every device and profile data.
Plexivo Auth signs your app’s users up and in, and keeps them in your own Plexivo Postgres database. Once a user signs in, every Data API, Realtime and GraphQL request runs as them, and your row-level security policies see their id as auth.uid().
Turn on sign-in#
- Turn on the Data API on the database’s Connect tab, if you have not already. See Data API.
- Open Authentication. Email and password, Email one-time code and Password recovery email are on from the start; the Providers view shows each method’s status.
- Open Sign-in methods to turn on Google, GitHub or Microsoft under Social sign-in, and Guests if you want them.
- Add your Return URLs: where your app may be sent back to after a social sign-in.
- Choose Save sign-in methods.
Sign up and sign in#
A new account is signed in at once. data is your own profile data for the user, such as a display name, kept as user_metadata:
import { createClient } from '@plexivo/client';
const db = createClient(PLEXIVO_DATA_URL, PLEXIVO_PUBLISHABLE_KEY);
const { user } = await db.auth.signUp({ email, password, data: { display_name: 'Asha' } });
console.log(user.id, user.user_metadata.display_name);
await db.auth.signInWithPassword({ email, password });import 'package:plexivo_client/plexivo_client.dart';
final db = PlexivoClient(dataUrl, publishableKey);
await db.signUp(email: email, password: password, data: {'display_name': 'Ravi'});
print(db.currentUser?['user_metadata']);
await db.signInWithPassword(email: email, password: password);Passwords are 10 to 128 bytes. A wrong email or password gives the same invalid_credentials, so nobody can learn who has an account.
The user and their profile data#
const me = await db.auth.getUser(); // fresh from the server
const session = db.auth.getSession(); // what this device holds
// Merge into user_metadata; a key set to null is removed
const updated = await db.auth.updateUser({ data: { theme: 'dark', display_name: null } });
console.log(updated.user_metadata);final me = await db.getUser();
final updated = await db.updateUser(data: {'theme': 'dark', 'display_name': null});Profile data holds up to 4 KB of JSON. Users can change their own, so never use it to decide what they may do: keep roles in your own tables, or in organisations.
Auth state#
Listen for changes to show the right screen. Events are SIGNED_UP, SIGNED_IN, TOKEN_REFRESHED, USER_UPDATED and SIGNED_OUT, among others:
const { data: { subscription } } = db.auth.onAuthStateChange((event, session) => {
console.log(event, session?.user?.email);
});final changes = db.authStateChanges.listen((state) {
print('${state.event} ${state.session?['user']?['email']}');
});Email codes#
Sign in without a password, or reset a forgotten one, with an 8-digit code sent by email. A code lasts 10 minutes and allows 5 tries; asking again replaces it. The request answers the same for any address, so nobody can learn who has an account.
await db.auth.signInWithOtp({ email });
await db.auth.resetPasswordForEmail(email);Then send the code the user types. A wrong or expired code is refused:
await db.auth.verifyOtp({ email, token: code });await db.auth.verifyPasswordRecovery({ email, code, newPassword });await db.signInWithOtp(email);
await db.verifyOtp(email: email, token: code);Google, GitHub and Microsoft#
Social sign-in uses your own OAuth app with each provider, so users see your app’s name.
- Create an OAuth app with the provider and register the redirect URI that the console shows under Social sign-in. It looks like
https://…/client/v1/<project>/auth/oauth/callback. - Paste its Client ID and Client secret under Social sign-in and turn the provider on. The secret is stored and never shown again.
- Add your app’s address to Return URLs: an exact
httpsaddress,http://localhostwhile you build, a path ending in*to allow everything below it, or your mobile app’s own scheme such asmyapp://auth. Up to 20.
Sign-in uses PKCE. In a browser, signInWithOAuth sends the user to the provider; when they come back to redirectTo with ?code=…, exchangeCodeForSession(code) signs them in. Elsewhere, open the URL yourself:
const { url, codeVerifier } = await db.auth.signInWithOAuth({
provider: 'github',
redirectTo: 'https://app.example.com/auth/done',
skipBrowserRedirect: true
});// With the code from the redirect; a code works once, within 60 seconds
await db.auth.exchangeCodeForSession(code, codeVerifier);final url = db.getOAuthSignInUrl(provider: 'google', redirectTo: 'myapp://auth');
// Open url in the browser; when the app opens again with ?code=…
await db.exchangeCodeForSession(code);If a provider says the email address is verified, the user joins the account that already has it.
Guests#
Turn on Guests under Sign-in methods to let people use your app before they sign up. A guest has a real user id, so their rows are theirs. When they add an email and password, they keep the same id and every row:
const guest = await db.auth.signInAnonymously();
console.log(guest.user.is_anonymous); // true
// Later, on the sign-up screen
const account = await db.auth.linkEmail({ email: 'asha.guest@example.com', password });
console.log(account.user.id === guest.user.id); // truefinal guest = await db.signInAnonymously();
final account = await db.linkEmail(email: 'ravi.guest@example.com', password: password);Policies can tell guests apart with (auth.jwt() ->> 'is_anonymous')::boolean; see Row-level security.
Sessions and devices#
const devices = await db.auth.listSessions(); // [{ id, user_agent, ip, last_active_at, current }]
for (const d of devices.filter((d) => !d.current)) await db.auth.revokeSession(d.id);
await db.auth.signOut(); // this devicefinal devices = await db.listSessions();
await db.signOut(allSessions: true); // every device- An access token lasts one hour by default. Set it from 5 to 1,440 minutes in Authentication → Sign-in methods → Sessions. The client libraries refresh it before it runs out, and once more if a request finds it expired.
- A refresh token lasts 30 days and works once: each refresh issues a new one.
- Ending a session, signing out, changing the password or disabling the user takes effect on the very next request, not when the token runs out.
- In a browser, the session is kept in
localStorage. In React Native and Flutter, keep it in encrypted storage: see the Flutter guide.
Two-step verification#
Users can add an authenticator app. After that, signing in also asks for its code, and a missing one is refused with mfa_required.
await db.auth.signInWithPassword({ email, password });
const factor = await db.auth.mfa.enroll(); // show factor.uri as a QR code
console.log(factor.status); // pending, until verify({ code })await db.signInWithPassword(email: email, password: password);
final factor = await db.mfa.enroll();Organisations#
For team apps, users can create organisations and invite others by email as admin or member. Invitations last 7 days, and only the invited address can accept. Policies check membership with auth.is_org_member(org) and auth.has_org_role(org, 'admin').
const team = await db.orgs.create({ name: 'Acme' });
const invite = await db.orgs.invite(team.id, { email: 'ben@acme.example', role: 'member' });
// Ben signs up with that address and accepts
const ben = createClient(PLEXIVO_DATA_URL, PLEXIVO_PUBLISHABLE_KEY, { persistSession: false });
await ben.auth.signUp({ email: 'ben@acme.example', password });
await ben.orgs.acceptInvitation(invite.token);
const members = await db.orgs.members(team.id);Users in your database#
Users live in your database, in a schema your app’s requests cannot read. Use auth.uid() and auth.email() in your own tables and policies, and keep public profile data in a table of your own, keyed by the user’s id. The Users view on the Authentication tab finds a user and can disable, enable or delete them.
Bring your users#
From your server, with the secret key, import users with their bcrypt password hashes, up to 1,000 per call, or every user of a migrated Supabase database. Their passwords keep working.
const server = createClient(process.env.PLEXIVO_DATA_URL, process.env.PLEXIVO_SECRET_KEY, { persistSession: false });
await server.auth.admin.importUsers([
{ email: 'meera@example.com', password_hash: '$2y$10$mYIYVb.I9kwHeVYhW0J8cuwKX09Y7FTkJT7IfQidoQJB8/hHXNd4i', email_verified: true }
]);Keys#
Rotate the publishable or secret key on the Authentication tab, under Keys. The old key stops working at once, so save the replacement and update your app first.
Next steps#
Updated 28 October 2026