"Subscriptions"
Subscribe to queries and receive realtime updates when data changes.
How subscriptions work
Tether uses WebSocket connections to push updates to clients. When you subscribe to a query, you receive the initial data immediately, then automatic updates whenever a mutation affects the subscribed data.
Flow:
- Client subscribes to query — the client opens a WebSocket connection and registers a subscription.
- Initial data returned — the server immediately responds with the current query result.
- Updates pushed on changes — whenever a mutation affects the subscribed data, the server pushes an update.
Who receives data
What a connection receives depends on how it authenticates when it opens:
| Connection | Receives |
|---|---|
Publishable key (?key=tthr_pk_...), with an optional end-user token |
Fresh results of deployed query functions, run as that user with RLS applied. Raw table queries get a refetch signal instead. |
Secret API key, or a dashboard or CLI session of a project member (?token=...) |
Full results, logs, and cron triggers for server connections |
| No credentials | A refetch signal (invalidate) when a subscribed query changes, never data |
A key only reaches its own environment. Function logs stream only to full-access connections in the same environment.
Subscribing in Vue/Nuxt
Use the useQuery composable for reactive subscriptions. It's auto-imported by the Nuxt module:
// This automatically subscribes and updates when data changes
const { data, isLoading, error } = useQuery('todos.list', { completed: false });
Subscribing in React
Use the vanilla client's subscribe method with React's useEffect. The React SDK is in development.
import { useEffect, useState } from 'react';
import { tether } from '../lib/tether';
function TodoList() {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = tether.subscribe(
'todos.list',
{ completed: false },
(data) => {
setTodos(data);
setLoading(false);
}
);
return () => unsubscribe();
}, []);
if (loading) return <p>Loading...</p>;
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
Manual subscriptions
For more control, use the low-level subscription API:
import { TetherClient } from '@tthr/client';
const client = new TetherClient({
url: 'https://tether-api.yourdomain.com',
projectId: 'your-project-id',
authToken: () => getAuthToken(), // optional
});
// Subscribe to a query
const unsubscribe = client.subscribe(
'todos.list',
{ completed: false },
(data) => {
console.log('Todos updated:', data);
}
);
// Unsubscribe when done
unsubscribe();
Update behaviour
Subscription updates are complete snapshots, not diffs. When data changes, you receive the full query result. This simplifies client-side state management and ensures consistency.
Limits
| Limit | Value |
|---|---|
| Subscriptions per connection | 200 |
| Connections per IP address | 50 per project, 200 across all projects |
| Message size | 64 KB |
| Client messages | 20 a second, with bursts up to 250. A connection that sends faster is closed. |
| Fresh results after one write | 100 subscription groups. Past that, subscribers are told to refetch instead. |
Connections are checked again every 5 minutes. When the key or token a connection opened with has been revoked or has expired, or its member has been removed, the connection is closed, and the client reconnects with whatever credentials it has now.
Connection resilience
Tether automatically handles connection issues with built-in resilience features.
Heartbeat
The client sends periodic ping messages every 30 seconds to detect connection health. If no response is received within 10 seconds, the client automatically reconnects.
Automatic reconnection
On disconnection, the client reconnects with exponential backoff: the wait doubles from 1 second up to 30 seconds, with a random part so clients that dropped together don't all return at once. It keeps trying until it's back, so a slow deploy never leaves a page disconnected for good. Calling disconnect() stops it.
Token renewal
A socket keeps the end-user token it connected with. To carry on past that token's exp, the client sends a fresh one over the open socket:
{ "type": "auth", "token": "<new JWT>" }
The server answers { "type": "authenticated", "expires_at": 1767225600 }, or an error with code AUTH_REJECTED when the token isn't valid or is for a different user (the socket keeps its old token; reconnect to switch user). The JavaScript client does this a minute before expiry.
If the token runs out anyway, the server closes the socket with code 4401 ("Auth expired") within about a minute of its exp, rather than leaving it receiving data under the old identity. Reconnect with a fresh token.
Sync on reconnect
When reconnecting, the client sends its lastSyncTime to receive any changes that occurred while disconnected. The server maintains a change queue with:
- Maximum 1000 entries per project
- Maximum age of 5 minutes
- Per-query isolation
For disconnections longer than 5 minutes, clients receive fresh data without change replay.
Versioning
Each data message includes a version number. Clients can detect missed updates if versions are non-sequential:
{
"type": "data",
"id": "sub1",
"data": [...],
"meta": { "version": 42 }
}
Seeing who is connected
The project Overview tab in the dashboard lists every open WebSocket connection for the selected environment, with its subscriptions and the signed-in user id. See Observability.