"Vue / Nuxt"
First-class support for Vue 3 and Nuxt with reactive composables.
Installation
npm install @tthr/vue
Setup
Nuxt
Add the module to your nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@tthr/vue/nuxt'],
tether: {
projectId: 'your-project-id',
verbose: false, // Enable debug logging (optional)
},
});
You can also configure via environment variables:
TETHER_PROJECT_ID=your-project-id
TETHER_URL=https://tether-api.strands.gg
TETHER_API_KEY=tthr_live_xxxxxxxxxxxxx
TETHER_VERBOSE=true # Enable debug logging
Vue 3
Create a Tether instance and provide it to your app:
import { createApp } from 'vue';
import { createTether } from '@tthr/vue';
import App from './App.vue';
const tether = createTether({
url: 'https://tether-api.yourdomain.com',
projectId: 'your-project-id',
authToken: () => getAuthToken(), // optional
});
const app = createApp(App);
app.use(tether);
app.mount('#app');
SPA mode (publishable key)
To call Tether functions directly from the browser without a backend proxy, use a publishable key instead of a secret API key:
const tether = createTether({
url: 'https://tether-api.yourdomain.com',
projectId: 'your-project-id',
publishableKey: 'tthr_pk_xxxxxxxxxxxxx',
authToken: () => getAuthToken(), // JWT from your auth provider
});
Publishable keys are safe for client-side code and are restricted by your domain allowlist. Function access levels control which functions are available -- see the Functions docs for details on public, authenticated, and internal access levels.
Composables
useQuery
Subscribe to a query with reactive updates. Data fetches on the server (SSR) and automatically subscribes to WebSocket updates on the client:
// With a query name string
const { data, isLoading, error, refetch } = useQuery('todos.list', { completed: false });
// Or with a generated API reference for full type safety
const { data, isLoading, error, refetch } = useQuery(api.todos.list, { completed: false });
// data is reactive and updates automatically via WebSocket
// refetch() manually re-fetches the data
You can also await useQuery to wait for the initial data to load. The returned refs still update reactively via WebSocket after that:
// Data is already populated when the promise resolves
const { data: todos } = await useQuery('todos.list', { completed: false });
useMutation
Execute mutations with loading state:
// With a mutation name string
const { mutate, isPending, error } = useMutation('todos.create');
// Or with a generated API reference
const { mutate, isPending, error } = useMutation(api.todos.create);
// In your template or handler
await mutate({ title: 'Buy milk' });
$query & $mutation
For one-shot async calls without reactive state or subscriptions. Use these in event handlers, composables, Pinia stores, or anywhere you just need the data:
// Fetch data directly — returns raw data, no refs
const messages = await $query(api.messages.listByChannel, { channelId });
// Execute a mutation directly — returns the result
const post = await $mutation(api.posts.create, { title: 'Hello' });
// Also works with string names
const todos = await $query('todos.list', { completed: false });
These are auto-imported by the Nuxt module. They proxy through the server route, so your API key stays secure.
useTetherSubscription
Subscribe to realtime updates for a specific query (client-side only in Nuxt):
const { isConnected } = useTetherSubscription(
'todos.list',
{ completed: false },
(data) => {
// Called whenever the query data changes
console.log('Todos updated:', data);
}
);
// isConnected tracks WebSocket connection status
The subscription automatically handles reconnection with exponential backoff and resumes updates when the browser tab becomes visible again.
Database proxy
When writing functions in tether/functions/, you get access to a typed database proxy via the db parameter. This provides a Prisma-style API for all your tables.
// tether/functions/todos.ts
import { query, mutation, z } from '../_generated/db';
export const list = query({
handler: async ({ db }) => {
return db.todos.findMany({
where: { completed: false },
orderBy: { createdAt: 'desc' },
limit: 50,
});
},
});
export const create = mutation({
args: z.object({ title: z.string().min(1) }),
handler: async ({ args, db }) => {
return db.todos.insert({
title: args.title,
completed: false,
});
},
});
export const complete = mutation({
args: z.object({ _id: z.string() }),
handler: async ({ args, db }) => {
// Update by _id (system-generated primary key)
return db.todos.update({
where: { _id: args._id },
data: { completed: true },
});
},
});
See the Database API for all available methods.
File storage
Storage operations require the Tether client instance. In Nuxt, use useTetherServer() in server routes, or create a TetherClient directly for client-side uploads.
Server-side (Nuxt API routes)
// server/api/upload.post.ts
const tether = useTetherServer();
export default defineEventHandler(async (event) => {
const formData = await readMultipartFormData(event);
const file = formData?.[0];
if (!file) throw createError({ statusCode: 400 });
const assetId = await tether.storage.upload(file.data, {
metadata: { alt: 'User photo' },
});
// Save assetId in your data
await tether.mutation('profile.update', { photoId: assetId });
return { assetId };
});
Client-side (direct client)
import { TetherClient } from '@tthr/client';
const client = new TetherClient({
url: 'https://tether-api.yourdomain.com',
projectId: 'your-project-id',
});
async function handleUpload(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
const assetId = await client.storage.upload(file, {
metadata: { alt: 'User photo' },
onProgress: ({ loaded, total }) => {
console.log(`${Math.round(loaded / total * 100)}%`);
}
});
}
// Get download URL (expires after 1 hour)
const photoUrl = await client.storage.getUrl(assetId);
// Get asset metadata
const metadata = await client.storage.getMetadata(assetId);
// Delete an asset
await client.storage.delete(assetId);
Listing assets
List all assets for the project:
const tether = useTetherServer();
// List all assets
const { assets, totalCount } = await tether.storage.list();
// With pagination and filtering
const images = await tether.storage.list({
limit: 20,
offset: 0,
contentType: 'image/'
});
Reactive file URLs
Combine useQuery with a server route that resolves asset URLs:
const { data: profile } = useQuery('profile.get');
const photoUrl = ref<string>('');
watch(() => profile.value?.photoId, async (photoId) => {
if (photoId) {
// Fetch signed URL from your own server route
const { url } = await $fetch(`/api/storage/${photoId}`);
photoUrl.value = url;
}
});
Server-side usage (Nuxt)
Access Tether from your Nuxt server endpoints for webhooks, third-party integrations, and any server-side logic. Import from @tthr/vue/server.
Option 1: Global configuration (recommended)
Configure once in a Nitro plugin, then use anywhere:
// server/plugins/tether.ts
import { configureTetherServer } from '@tthr/vue/server';
export default defineNitroPlugin(() => {
configureTetherServer({
url: process.env.TETHER_URL!,
projectId: process.env.TETHER_PROJECT_ID!,
apiKey: process.env.TETHER_API_KEY,
});
});
Then use in any API route:
// server/api/webhook.post.ts
import { useTetherServer } from '@tthr/vue/server';
export default defineEventHandler(async (event) => {
const tether = useTetherServer();
// Execute queries
const users = await tether.query('users.list');
// Execute mutations
await tether.mutation('users.update', { id: 1, status: 'active' });
// Execute actions (external side effects)
await tether.action('emails.sendWelcome', { userId: 1 });
// Access storage
const url = await tether.storage.getUrl(assetId);
return { ok: true };
});
Option 2: Standalone client
Create a client instance directly when you need different configuration:
// server/api/external-webhook.ts
import { createTetherServer } from '@tthr/vue/server';
const tether = createTetherServer({
url: process.env.TETHER_URL!,
projectId: process.env.TETHER_PROJECT_ID!,
apiKey: process.env.TETHER_API_KEY,
});
export default defineEventHandler(async (event) => {
const body = await readBody(event);
await tether.mutation('webhooks.process', body);
return { received: true };
});
Environment variables
Set your credentials in your .env file:
TETHER_URL=https://tether-api.yourdomain.com
TETHER_PROJECT_ID=your-project-id
TETHER_API_KEY=tthr_live_xxxxxxxxxxxxx
The API key is only available server-side and is never exposed to the client. Use tthr_live_ keys for production and tthr_dev_ keys for development.
Server client vs browser client
The server client uses HTTP-only requests and does not establish WebSocket connections. This makes it lightweight and suitable for serverless environments like Nuxt API routes, Vercel functions, or Cloudflare Workers.
// Available methods on server client:
tether.query(name, args) // Read data
tether.mutation(name, args) // Write data
tether.action(name, args) // External side effects
tether.storage.* // Full storage API
TypeScript
The SDK is fully typed. Run tthr generate to create types from your schema for full autocomplete on queries and mutations.