Tokens are stored in localStorage (browser) or secure storage (mobile):
// Client automatically restores session on initializationconst client = initClient("http://localhost:4000");// User is automatically logged in if valid tokens existif (client.user()) { console.log("Restored session for:", client.user()?.email);}
import { initClient } from "trailbase";const client = initClient("http://localhost:4000");// Redirect to OAuth providerfunction loginWithGitHub() { window.location.href = client.oauthUrl("github", { redirect_uri: window.location.origin + "/callback", });}// Handle callback on returnconst params = new URLSearchParams(window.location.search);if (params.has("code")) { // TrailBase automatically handles the OAuth callback // and sets the auth tokens console.log("Logged in:", client.user());}
import 'package:trailbase/trailbase.dart';import 'package:uni_links/uni_links.dart';final client = Client("http://localhost:4000");// Start OAuth flow with PKCEFuture<void> loginWithGitHub() async { // Generate PKCE challenge final pkce = await client.generatePkce(); // Open OAuth URL in browser final url = client.oauthUrl( "github", redirectUri: "myapp://auth/callback", pkceChallenge: pkce.challenge, ); await launchUrl(Uri.parse(url)); // Listen for deep link callback final uri = await getInitialUri(); if (uri != null && uri.queryParameters.containsKey('code')) { final code = uri.queryParameters['code']!; // Exchange code for tokens await client.oauthCallback( code: code, codeVerifier: pkce.verifier, ); print("Logged in: ${client.user()}"); }}
<!-- Login button that redirects to OAuth provider --><a href="http://localhost:4000/_/auth/login?provider=github&redirect_uri=http://localhost:3000/callback"> Login with GitHub</a><!-- Or using the login UI --><a href="http://localhost:4000/_/auth/login"> Login</a>
Mobile Apps: Use PKCE (Proof Key for Code Exchange) for secure OAuth flows without exposing client secrets.
# Verify a usertrail user verify[email protected] true# List unverified userssqlite3 traildepot/data/main.db "SELECT email FROM _user WHERE verified = 0;"
record_apis: [ { name: "todos" table_name: "todos" autofill_missing_user_id_columns: true acl_authenticated: [CREATE, READ, UPDATE, DELETE] # Users can only see their own todos read_access_rule: "_ROW_.user = _USER_.id" # Users can only update their own todos update_access_rule: "_ROW_.user = _USER_.id" # Users can only delete their own todos delete_access_rule: "_ROW_.user = _USER_.id" }]
record_apis: [ { name: "articles" table_name: "articles" acl_authenticated: [CREATE, READ, UPDATE, DELETE] # Only editors can create articles create_access_rule: "EXISTS(SELECT * FROM editors WHERE user = _USER_.id)" # Authors can update their own articles update_access_rule: "_ROW_.author = _USER_.id AND EXISTS(SELECT * FROM editors WHERE user = _USER_.id)" # Authors can delete their own articles delete_access_rule: "_ROW_.author = _USER_.id" }]
Access Rule Variables:
_USER_.id - Current user’s ID
_USER_.email - Current user’s email
_USER_.admin - Whether user is admin
_ROW_.* - Column values from the row being accessed
_REQ_.* - Values from the request body (for CREATE/UPDATE)
CREATE TABLE profiles ( user BLOB PRIMARY KEY NOT NULL REFERENCES _user(id) ON DELETE CASCADE, username TEXT NOT NULL CHECK(username REGEXP '^[\w]{3,}$'), bio TEXT, avatar_url TEXT, created INTEGER DEFAULT (UNIXEPOCH()) NOT NULL, updated INTEGER DEFAULT (UNIXEPOCH()) NOT NULL) STRICT;CREATE UNIQUE INDEX _profiles__username_index ON profiles (username);CREATE TRIGGER _profiles__updated_trigger AFTER UPDATE ON profiles FOR EACH ROW BEGIN UPDATE profiles SET updated = UNIXEPOCH() WHERE user = OLD.user; END;
Expose via API:
traildepot/config.textproto
record_apis: [ { name: "profiles" table_name: "profiles" acl_authenticated: [CREATE, READ] acl_world: [READ] # Users can only create their own profile create_access_rule: "_REQ_.user = _USER_.id" }]