Sessions & Authentication
Ductape sessions are JWT-based tokens that identify a user across product operations. Starting, verifying, refreshing, and revoking sessions is backend-only — use @ductape/sdk with an access key on your API or BFF.
The browser uses a publishable key and never calls session lifecycle APIs. After login, your backend returns a session token; the frontend passes it as session on every Ductape request.
How it works
- User signs in through your auth (email/password, OAuth, etc.).
- Your backend validates credentials and calls
ductape.sessions.start()with@ductape/sdk. - Your API returns
token(formattag:jwt) and optionallyrefreshTokento the client. - The frontend stores the token and includes
session: tokenon databases, storage, actions, and other calls. - Refresh and revoke stay on the backend — expose your own
/auth/refreshand/auth/logoutroutes that call the SDK.
Starting a session (backend only)
@ductape/sdkSession lifecycle methods (sessions.start, sessions.verify, sessions.refresh, sessions.revoke, etc.) require an access key and must run on your server. They are not available from @ductape/client, @ductape/react, or @ductape/vue when using a publishable key.
import Ductape from '@ductape/sdk';
const ductape = new Ductape({
accessKey: process.env.DUCTAPE_ACCESS_KEY!,
product: 'my-product',
env: 'prd',
});
// After your own auth validates the user
const result = await ductape.sessions.start({
tag: 'user-session',
data: {
userId: 'user_123',
details: {
email: 'user@example.com',
role: 'user',
},
},
});
// Return to the browser via your login API
res.json({
sessionToken: result.token,
refreshToken: result.refreshToken,
expiresAt: result.expiresAt,
});
Custom duration and session types
// Longer-lived admin session
const adminSession = await ductape.sessions.start({
tag: 'admin-session',
data: {
userId: 'admin_456',
details: { email: 'admin@company.com', role: 'admin' },
},
expiresIn: 86400, // 24 hours
});
// Short-lived checkout flow
const checkoutSession = await ductape.sessions.start({
tag: 'checkout-session',
data: {
userId: 'user_123',
details: { cartId: 'cart_789' },
},
expiresIn: 1800, // 30 minutes
});
Verifying and refreshing (backend only)
Verify a token when your API needs to read encrypted session data or check expiry:
const verified = await ductape.sessions.verify({
tag: 'user-session',
token: sessionTokenFromClient,
});
console.log(verified.data.userId);
console.log(verified.expiresAt);
Refresh before expiry — call from a backend route, not from the browser SDK:
const renewed = await ductape.sessions.refresh({
tag: 'user-session',
refreshToken: refreshTokenFromClient,
});
// Return new tokens to the client
res.json({ sessionToken: renewed.token, refreshToken: renewed.refreshToken });
Revoking sessions (backend only)
// Logout — revoke the current session
const verified = await ductape.sessions.verify({
tag: 'user-session',
token: sessionToken,
});
await ductape.sessions.revoke({
tag: 'user-session',
sessionId: verified.sessionId,
});
// Log out all devices for a user
await ductape.sessions.revokeAll({
tag: 'user-session',
identifier: 'user_123',
});
Using session tokens on the frontend
Once your backend returns a session token, pass it on every Ductape call. With a publishable key, requests without session are rejected.
- @ductape/client
- React
- Vue 3
Store the token after login
Your login flow calls your backend; the backend starts the Ductape session and returns the token:
// Your app's login handler (browser)
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const { sessionToken, refreshToken } = await res.json();
localStorage.setItem('ductape_session', sessionToken);
localStorage.setItem('ductape_refresh', refreshToken);
Pass session on every request
import { Ductape } from '@ductape/client';
const ductape = new Ductape({
publishableKey: 'pk_…',
product: 'my-product',
env: 'prd',
});
function getSession(): string {
const token = localStorage.getItem('ductape_session');
if (!token) throw new Error('Not authenticated');
return token;
}
// Databases
const users = await ductape.databases.query({
table: 'users',
session: getSession(),
});
// Storage
await ductape.storage.upload({
bucket: 'avatars',
key: 'user-123.png',
file: blob,
session: getSession(),
});
// Actions
await ductape.actions.run({
action: 'send-welcome-email',
session: getSession(),
input: { userId: 'user_123' },
});
Refresh and logout via your backend
Do not call ductape.sessions.refresh() from the browser. Proxy through your API:
async function refreshSession() {
const refreshToken = localStorage.getItem('ductape_refresh');
const res = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
const { sessionToken, refreshToken: newRefresh } = await res.json();
localStorage.setItem('ductape_session', sessionToken);
localStorage.setItem('ductape_refresh', newRefresh);
}
async function logout() {
await fetch('/api/auth/logout', {
method: 'POST',
headers: {
Authorization: `Bearer ${localStorage.getItem('ductape_session')}`,
},
});
localStorage.removeItem('ductape_session');
localStorage.removeItem('ductape_refresh');
}
Analytics
After login, attach the session to product analytics events:
ductape.analytics.identify(sessionToken);
Session lifecycle hooks (useSessionStart, useSessionVerify, useSessionRefresh, etc.) are not available with a publishable key. Start sessions on your backend with @ductape/sdk, then use the token in frontend hooks.
Auth context pattern
import { createContext, useContext, useState, useCallback } from 'react';
import { DuctapeProvider } from '@ductape/react';
type AuthContextType = {
sessionToken: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
};
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [sessionToken, setSessionToken] = useState<string | null>(
() => localStorage.getItem('ductape_session'),
);
const login = useCallback(async (email: string, password: string) => {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const { sessionToken: token, refreshToken } = await res.json();
localStorage.setItem('ductape_session', token);
localStorage.setItem('ductape_refresh', refreshToken);
setSessionToken(token);
}, []);
const logout = useCallback(async () => {
await fetch('/api/auth/logout', { method: 'POST' });
localStorage.removeItem('ductape_session');
localStorage.removeItem('ductape_refresh');
setSessionToken(null);
}, []);
return (
<AuthContext.Provider value={{ sessionToken, login, logout }}>
<DuctapeProvider
config={{
publishableKey: import.meta.env.VITE_DUCTAPE_PK,
product: 'my-product',
}}
>
{children}
</DuctapeProvider>
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth requires AuthProvider');
return ctx;
}
Pass session in hooks
import { useDatabaseQuery } from '@ductape/react';
import { useAuth } from './AuthProvider';
function UsersList() {
const { sessionToken } = useAuth();
const { data, isLoading, error } = useDatabaseQuery('users', {
table: 'users',
limit: 20,
session: sessionToken!, // required with publishable key
}, { enabled: !!sessionToken });
if (!sessionToken) return <p>Please log in.</p>;
if (isLoading) return <p>Loading…</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data?.rows?.map((row) => (
<li key={row.id}>{row.email}</li>
))}
</ul>
);
}
The same session option applies to useStorageUpload, useActionRun, feature hooks, and other Ductape React hooks.
Session lifecycle composables (useSessionStart, useSessionVerify, useSessionRefresh, etc.) are not available with a publishable key. Start sessions on your backend with @ductape/sdk, then pass the token in frontend composables.
Auth composable
<script setup lang="ts">
import { ref, provide, inject, type InjectionKey } from 'vue';
type AuthContext = {
sessionToken: Ref<string | null>;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
};
const authKey: InjectionKey<AuthContext> = Symbol('auth');
const sessionToken = ref<string | null>(localStorage.getItem('ductape_session'));
async function login(email: string, password: string) {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const { sessionToken: token, refreshToken } = await res.json();
localStorage.setItem('ductape_session', token);
localStorage.setItem('ductape_refresh', refreshToken);
sessionToken.value = token;
}
async function logout() {
await fetch('/api/auth/logout', { method: 'POST' });
localStorage.removeItem('ductape_session');
localStorage.removeItem('ductape_refresh');
sessionToken.value = null;
}
provide(authKey, { sessionToken, login, logout });
</script>
Pass session in composables
<script setup lang="ts">
import { computed } from 'vue';
import { useDatabaseQuery } from '@ductape/vue';
import { useAuth } from './useAuth'; // injects sessionToken from provider
const { sessionToken } = useAuth();
const { data, isLoading, error } = useDatabaseQuery(
'users',
computed(() => ({
table: 'users',
limit: 20,
session: sessionToken.value!,
})),
{ enabled: computed(() => !!sessionToken.value) },
);
</script>
<template>
<p v-if="!sessionToken">Please log in.</p>
<p v-else-if="isLoading">Loading…</p>
<p v-else-if="error">{{ error.message }}</p>
<ul v-else>
<li v-for="row in data?.rows" :key="row.id">{{ row.email }}</li>
</ul>
</template>
The same session option applies to useStorageUpload, useActionRun, feature composables, and other Ductape Vue helpers.
See also
- Generating session tokens — full backend reference
- Refreshing session tokens
- Sessions overview
- Product Analytics —
identify(sessionToken)for frontend events