"Direct Access"
Connect directly to your environment database using `@libsql/client` for advanced control over tables, data, and schema beyond what the CRUD API provides.
Overview
Tether's Database API and Functions handle most use cases, but sometimes you need lower-level access — managing tables, running migrations, bulk operations, or querying data in ways the CRUD API doesn't support. Direct Access lets you run raw SQL against your environment database using the standard @libsql/client library.
Each connection is scoped to a single environment and authenticated with a dedicated database token that has granular permissions.
Creating a database token
Database tokens are managed per environment from the Tether dashboard or via the API. Each token has a name, a set of permissions, and an optional expiry date.
Available permissions
| Permission | Description |
|---|---|
read |
SELECT queries and reading data |
write |
INSERT and UPDATE statements |
delete |
DELETE statements |
schema |
CREATE TABLE, ALTER TABLE, DROP TABLE, and access to internal Tether tables |
pragma |
PRAGMA statements for database configuration |
Via the API
// Create a token with specific permissions
const response = await fetch(
'https://api.tetherdb.co.uk/api/v1/projects/{projectId}/env/{envName}/db-tokens',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
},
body: JSON.stringify({
name: 'Admin token',
permissions: ['read', 'write', 'delete', 'schema'],
expires_at: '2026-12-31T23:59:59Z', // optional
}),
}
);
const { raw_token, connection_url } = await response.json();
// raw_token: "tthr_db_..." — store this securely, it's only shown once
// connection_url: "https://api.tetherdb.co.uk/api/v1/projects/{id}/env/{env}"
Important: The raw token is only returned once at creation time. Store it securely — Tether only keeps a hash of the token and cannot retrieve it later.
Connecting with @libsql/client
Install the libSQL client and connect using the token and URL from the previous step.
npm install @libsql/client
import { createClient } from '@libsql/client';
const db = createClient({
url: 'https://api.tetherdb.co.uk/api/v1/projects/{projectId}/env/{envName}',
authToken: 'tthr_db_...',
});
// Run queries
const result = await db.execute('SELECT * FROM users');
console.log(result.rows);
// Insert data
await db.execute({
sql: 'INSERT INTO users (name, email) VALUES (?, ?)',
args: ['Alice', '[email protected]'],
});
// Run migrations
await db.execute(`
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
action TEXT NOT NULL,
user_id TEXT,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// Batch operations
await db.batch([
{ sql: 'INSERT INTO tags (name) VALUES (?)', args: ['typescript'] },
{ sql: 'INSERT INTO tags (name) VALUES (?)', args: ['sqlite'] },
{ sql: 'INSERT INTO tags (name) VALUES (?)', args: ['realtime'] },
]);
Permission enforcement
Every SQL statement is classified and checked against the token's permissions before execution. If a statement requires a permission the token doesn't have, the request is rejected with a PERMISSION_DENIED error.
// Token with only "read" permission
const db = createClient({ url, authToken: readOnlyToken });
// This works
await db.execute('SELECT * FROM users');
// This fails with PERMISSION_DENIED
await db.execute('INSERT INTO users (name) VALUES (?)', ['Bob']);
// Error: Permission denied: 'write' permission required for this statement
Internal table protection
Tables prefixed with _tether_ are internal to the Tether system and are protected by default. Only tokens with the schema permission can access them.
SQLite itself also checks every statement against the token's permissions, so a statement that looks like a read but writes (a WITH clause in front of a DELETE, say) is still refused. The internal table check is case-insensitive.
Always blocked
These are refused for every token, whatever its permissions:
ATTACHandDETACH, which could open another database fileVACUUM, includingVACUUM INTOPRAGMA writable_schema
Limits
| Limit | Value |
|---|---|
| Requests per pipeline | 200 |
| Result size | 50,000 rows per statement, about 64 MB per pipeline |
| Columns in a result | 64 |
| A single text or blob value | 512 KB |
| Time | 30 seconds per pipeline |
| Pipelines at once | 3 per project |
The result size counts each value as it's sent: escaped characters in text count up to six times, blobs count as base64, and every value adds about 100 bytes. A result over a limit fails with RESULT_TOO_LARGE; add LIMIT and page through it. The limits apply to values as they're read, so a row holding a value over 512 KB, or a SELECT * on a table wider than 64 columns, can't be read here; use your functions for those.
With 3 pipelines already running for the project, or the server busy, a pipeline waits up to 10 seconds to start, then gets 503; retry shortly. Waiting doesn't count towards the 30 seconds.
Managing tokens
List tokens
GET /api/v1/projects/{projectId}/env/{envName}/db-tokens
Response:
{
"tokens": [
{
"id": "token-uuid",
"name": "Admin token",
"permissions": ["read", "write", "delete", "schema"],
"created_by": "user-id",
"created_at": "2026-01-15T10:30:00Z",
"expires_at": null,
"last_used_at": "2026-03-07T14:22:00Z",
"revoked": false
}
]
}
Update a token
PATCH /api/v1/projects/{projectId}/env/{envName}/db-tokens/{tokenId}
{
"name": "Read-only token",
"permissions": ["read"]
}
Revoke a token
DELETE /api/v1/projects/{projectId}/env/{envName}/db-tokens/{tokenId}
Response:
{ "revoked": true }
Revoked tokens are immediately invalidated. Any connections using a revoked token will be rejected on the next request.
Realtime compatibility
Changes made through Direct Access automatically trigger realtime subscription updates. If you modify a table that has active WebSocket subscribers, they'll receive invalidation signals and re-fetch their data — just as if the change came through the CRUD API.
Use cases
| Use case | Description |
|---|---|
| Migrations | Create, alter, or drop tables beyond what the schema file supports |
| Bulk operations | Import or transform large datasets with raw SQL |
| Complex queries | JOINs, subqueries, CTEs, and aggregations not available via the CRUD API |
| Admin tooling | Build custom admin panels or scripts with full database access |
| Debugging | Inspect and fix data directly during development |