Authentication & Security
Secure your local-first application by integrating authentication providers and managing access control.
Token-Based
Standard JWT authentication integrated directly into the sync protocol.
Provider Agnostic
Works with Clerk, Better Auth, Auth0, Firebase, or any JWT-based provider.
Access Control
Fine-grained RBAC permissions per collection with pattern matching.
How it Works
In a local-first architecture, authentication serves two main purposes:
- Gatekeeping Sync: Only authenticated users can connect to the synchronization server (WebSocket).
- Authorization: Ensuring users can only read or write data they are permitted to access.
TopGun decouples the authentication provider from the sync engine. You can use any provider to generate a JWT token. This token is passed to the TopGun client, which sends it to the server during the WebSocket handshake.
Integration Flow
Authenticate User
Use your preferred provider SDK to log the user in and retrieve a session token (JWT).
Set Token in Client
Pass the token to the TopGun client. The client will automatically authenticate the WebSocket connection.
Server Validation
The TopGun server validates the JWT using the configured secret. If valid, the connection is established with the user's principal attached.
Clerk Integration
1. Setup TopGun Client
import { TopGunClient } from '@topgunbuild/client';
import { IDBAdapter } from '@topgunbuild/adapters';
const adapter = new IDBAdapter();
export const tgClient = new TopGunClient({
serverUrl: 'ws://localhost:8090',
storage: adapter
}); 2. Create Auth Sync Component
Use setAuthTokenProvider to automatically refresh tokens when they expire.
import { useEffect } from 'react';
import { useAuth } from '@clerk/clerk-react';
import { tgClient } from '../lib/topgun';
export function TopGunAuthSync() {
const { getToken, isSignedIn } = useAuth();
useEffect(() => {
if (isSignedIn) {
// Set a token provider that refreshes automatically
tgClient.setAuthTokenProvider(async () => {
try {
const token = await getToken();
return token;
} catch (err) {
console.error('Failed to get Clerk token', err);
return null;
}
});
}
}, [isSignedIn, getToken]);
return null;
} 3. Wire Up in App
import { ClerkProvider, SignedIn, SignedOut, SignIn } from '@clerk/clerk-react';
import { TopGunProvider } from '@topgunbuild/react';
import { TopGunAuthSync } from './components/TopGunAuthSync';
import { tgClient } from './lib/topgun';
const CLERK_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
export default function App() {
return (
<ClerkProvider publishableKey={CLERK_KEY}>
<TopGunProvider client={tgClient}>
<TopGunAuthSync />
<SignedIn>
{/* Your authenticated app */}
<Dashboard />
</SignedIn>
<SignedOut>
<SignIn />
</SignedOut>
</TopGunProvider>
</ClerkProvider>
);
} 4. User-Scoped Data
Use the user ID from Clerk to scope data per user:
import { useUser } from '@clerk/clerk-react';
import { useQuery } from '@topgunbuild/react';
export function UserNotes() {
const { user } = useUser();
// Each user gets their own notes collection
const mapName = `notes:${user?.id}`;
const { data: notes } = useQuery(mapName);
return (
<ul>
{notes.map(note => (
<li key={note.id}>{note.title}</li>
))}
</ul>
);
} Better Auth Integration
Note: This approach stores auth data (users, sessions) in TopGun itself, allowing authentication data to be distributed and synced like any other application data.
1. Install the Adapter
npm install @topgunbuild/adapter-better-auth better-auth 2. Configure Better Auth
import { betterAuth } from 'better-auth';
import { topGunAdapter } from '@topgunbuild/adapter-better-auth';
import { tgClient } from './topgun';
export const auth = betterAuth({
database: topGunAdapter({
client: tgClient,
// Optional: customize collection names
modelMap: {
user: 'auth_user',
session: 'auth_session',
account: 'auth_account',
verification: 'auth_verification'
}
}),
// Configure your auth methods
emailAndPassword: {
enabled: true
}
}); 3. Use in Your App
import { auth } from '../lib/auth';
export function SignUpForm() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const form = new FormData(e.target as HTMLFormElement);
await auth.signUp.email({
email: form.get('email') as string,
password: form.get('password') as string,
name: form.get('name') as string
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" />
<input name="email" type="email" placeholder="Email" />
<input name="password" type="password" placeholder="Password" />
<button type="submit">Sign Up</button>
</form>
);
} Server Configuration
The TopGun server validates JWT tokens using a shared secret. Configure the secret to match your auth provider.
import { ServerCoordinator } from '@topgunbuild/server';
const server = new ServerCoordinator({
port: 8090,
nodeId: 'node-1',
// JWT secret for token verification
// Use the same secret your auth provider uses to sign tokens
jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
// Optional: RBAC security policies
securityPolicies: [
{
role: 'USER',
mapNamePattern: 'notes:{userId}/*', // {userId} is replaced with the authenticated user's ID
actions: ['READ', 'PUT']
},
{
role: 'ADMIN',
mapNamePattern: '*',
actions: ['ALL']
}
]
});
server.start();
console.log('TopGun server running on port 8090'); JWT Token Structure
TopGun expects the following claims in the JWT payload:
{
"sub": "user_123", // User ID (or use "userId")
"userId": "user_123", // Alternative to "sub"
"roles": ["USER"], // Array of roles for RBAC
"iat": 1699000000, // Issued at
"exp": 1699086400 // Expiration
} Custom JWT Provider
If you’re using a custom authentication system, you can generate JWT tokens manually.
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
export function generateToken(user: { id: string; roles: string[] }) {
return jwt.sign(
{
sub: user.id,
userId: user.id,
roles: user.roles
},
JWT_SECRET,
{ expiresIn: '24h' }
);
}
// In your login endpoint
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
// Verify credentials...
const user = await verifyCredentials(email, password);
if (user) {
const token = generateToken(user);
res.json({ token });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
}); Client-Side Usage
import { tgClient } from './topgun';
export async function login(email: string, password: string) {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const { token } = await res.json();
// Set the token in TopGun client
tgClient.setAuthToken(token);
// Store for persistence across page reloads
localStorage.setItem('topgun_token', token);
}
// On app init, restore token
const savedToken = localStorage.getItem('topgun_token');
if (savedToken) {
tgClient.setAuthToken(savedToken);
} Production Deployment with Clerk
Important: Clerk uses RS256 (asymmetric RSA) algorithm for JWT signing, not HS256. This means you need to use Clerk’s public key for token verification on the server.
Getting the Clerk Public Key
Clerk publishes its public keys via the JWKS (JSON Web Key Set) endpoint. You can find your instance’s public key at:
https://YOUR_CLERK_DOMAIN.clerk.accounts.dev/.well-known/jwks.json
// Fetch JWKS from Clerk
const response = await fetch('https://YOUR_CLERK_DOMAIN.clerk.accounts.dev/.well-known/jwks.json');
const jwks = await response.json();
// The response contains the public key in JWK format
// You'll need to convert it to PEM format for JWT_SECRET
// Use a tool like https://8gwifi.org/jwkconvertfunctions.jsp
// or the 'jwk-to-pem' npm package Configuring JWT_SECRET for Docker/Dokploy
When deploying to Docker-based platforms (Dokploy, Railway, etc.), you need to handle the PEM key format carefully:
# 1. Get your Clerk Public Key from the JWKS endpoint
# Visit: https://YOUR_CLERK_DOMAIN.clerk.accounts.dev/.well-known/jwks.json
# Copy the RSA public key (starts with -----BEGIN PUBLIC KEY-----)
# 2. Set the JWT_SECRET environment variable
# In Docker/Dokploy, use escaped newlines:
JWT_SECRET="-----BEGIN PUBLIC KEY-----\nMIIBIjAN...your-key...AQAB\n-----END PUBLIC KEY-----"
# Or in a shell script, use real newlines:
export JWT_SECRET="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----" Note: The TopGun server automatically detects RSA public keys (by checking for -----BEGIN) and uses the RS256 algorithm. It also handles escaped newlines (\n) from Docker environment variables.
Algorithm Support
TopGun server supports both symmetric and asymmetric JWT algorithms:
| Provider | Algorithm | JWT_SECRET Value |
|---|---|---|
| Custom/Self-hosted | HS256 | Shared secret string |
| Clerk | RS256 | RSA Public Key (PEM) |
| Auth0 | RS256 | RSA Public Key (PEM) |
| Firebase | RS256 | RSA Public Key (PEM) |
Authentication Protocol
Here’s how the authentication handshake works at the WebSocket level:
AUTH_REQUIREDAUTH + JWT tokenAUTH_ACK (success) or close connection (failure)