DocsGuidesAuthentication

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

1

Authenticate User

Use your preferred provider SDK to log the user in and retrieve a session token (JWT).

2

Set Token in Client

Pass the token to the TopGun client. The client will automatically authenticate the WebSocket connection.

3

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

Clerk is a popular authentication provider with excellent React support. Here’s how to integrate it with TopGun.

1. Setup TopGun Client

src/lib/topgun.ts
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.

src/components/TopGunAuthSync.tsx
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

src/App.tsx
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:

src/components/UserNotes.tsx
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

Better Auth is a framework-agnostic authentication library. TopGun provides a dedicated adapter that uses TopGun as the database backend for Better Auth.

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

Terminal
npm install @topgunbuild/adapter-better-auth better-auth

2. Configure Better Auth

src/lib/auth.ts
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

src/components/AuthForm.tsx
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.

server/index.ts
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:

Expected 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.

server/auth.ts
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

src/lib/auth.ts
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
Fetching JWKS
// 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:

Environment Configuration
# 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:

ProviderAlgorithmJWT_SECRET Value
Custom/Self-hostedHS256Shared secret string
ClerkRS256RSA Public Key (PEM)
Auth0RS256RSA Public Key (PEM)
FirebaseRS256RSA Public Key (PEM)

Authentication Protocol

Here’s how the authentication handshake works at the WebSocket level:

ClientConnect to WebSocket
ServerAUTH_REQUIRED
ClientAUTH + JWT token
ServerVerify JWT, extract principal
ServerAUTH_ACK (success) or close connection (failure)