DEX provides a type-safe API built with tRPC for client-server communication. This document covers all available endpoints and their usage.
Base URL: {BACKEND_URL}/api/trpc
Protocol: tRPC (Type-safe Remote Procedure Call)
Authentication: Session-based via Better Auth or API Key (X-API-Key header)
Content Type: application/json
DEX uses Better Auth for session-based authentication:
Endpoints:
POST /api/auth/sign-in/google- Initiate Google OAuth flowPOST /api/auth/sign-out- Sign out and destroy sessionGET /api/auth/session- Get current user session
Flow:
- User clicks "Sign in with Google"
- Redirected to Google OAuth consent screen
- Google redirects back with authorization code
- Backend exchanges code for tokens and creates session
- Session cookie stored in browser
- Subsequent requests include session cookie automatically
For programmatic access (e.g., MCP server, CLI tools):
Header Format:
X-API-Key: dex_live_abc123...
Creating API Keys:
Use the tRPC apiKeys.create mutation or the web interface.
API Key Modes:
-
Full Access Mode
- Access to all user collections
- Can create new collections
- Ideal for personal automation
-
Collection-Specific Mode
- Access limited to specified collections
- Cannot create new collections
- Ideal for third-party integrations
Manage bookmarks and notes.
Create a new item.
Type: mutation
Input:
{
url: string; // URL to save
collectionId?: string; // Optional: add to collection immediately
}Returns:
{
id: string;
url: string;
title: string;
tldr: string | null;
tags: string[];
favicon: string | null;
image: string | null;
creatorId: string;
createdAt: Date;
updatedAt: Date;
}Example:
const item = await trpc.items.create.mutate({
url: "https://example.com/article",
collectionId: "col_123",
});Update an existing item.
Type: mutation
Input:
{
id: string;
title?: string;
tldr?: string;
tags?: string[];
}Returns: Updated item object
Full-text search across all items.
Type: query
Input:
{
query: string; // Search query
}Returns: Array of matching items with relevance ranking
Example:
const results = await trpc.items.search.useQuery({
query: "javascript tutorial",
});Get recently created items.
Type: query
Input:
{
limit?: number; // Default: 10
}Returns: Array of items, sorted by creation date (newest first)
Check if a URL already exists in user's collections.
Type: query
Input:
{
url: string; // Valid URL
}Returns:
{
exists: boolean;
collections: Array<{
id: string;
title: string;
}>;
}Manage collections of items.
Get all collections accessible by the current user.
Type: query
Input:
{
sortBy?: 'createdAt' | 'updatedAt' | 'title';
order?: 'asc' | 'desc';
}Returns:
Array<{
id: string;
title: string;
createdAt: Date;
updatedAt: Date;
role: "owner" | "admin" | "member";
isShared: boolean;
memberCount: number;
}>;Get a single collection with all its items.
Type: query
Input:
{
id: string; // Collection ID
}Returns:
{
id: string;
title: string;
createdAt: Date;
updatedAt: Date;
items: Array<Item>;
members: Array<{
userId: string;
role: "owner" | "admin" | "member";
}>;
}Create a new collection.
Type: mutation
Input:
{
title: string; // Collection name
}Returns: Created collection object
Update collection details.
Type: mutation
Input:
{
id: string;
title: string;
}Returns: Updated collection object
Permissions: Owner or Admin
Delete a collection and all its contents.
Type: mutation
Input:
{
id: string;
}Returns: Success confirmation
Permissions: Owner only
Add an existing item to a collection.
Type: mutation
Input:
{
collectionId: string;
itemId: string;
}Returns: Success confirmation
Remove an item from a collection.
Type: mutation
Input:
{
collectionId: string;
itemId: string;
}Returns: Success confirmation
Note: This doesn't delete the item, just removes it from the collection.
Copy an item from one collection to another.
Type: mutation
Input:
{
itemId: string;
fromCollectionId: string;
toCollectionId: string;
}Returns: Success confirmation
Note: Item exists in both collections after this operation.
Move an item from one collection to another.
Type: mutation
Input:
{
itemId: string;
fromCollectionId: string;
toCollectionId: string;
}Returns: Success confirmation
Note: Item is removed from source collection.
Get count of collections and total items.
Type: query
Input: None
Returns:
{
collectionsCount: number;
itemsCount: number;
}Manage collection sharing and permissions.
Get all members of a collection.
Type: query
Input:
{
collectionId: string;
}Returns:
Array<{
userId: string;
userName: string;
userEmail: string;
userImage: string | null;
role: "owner" | "admin" | "member";
}>;Add a new member to a collection.
Type: mutation
Input:
{
collectionId: string;
userId: string;
role: "admin" | "member";
}Returns: Success confirmation
Permissions: Owner or Admin
Note: Cannot add users as owner (use transferOwnership instead).
Remove a member from a collection.
Type: mutation
Input:
{
collectionId: string;
userId: string;
}Returns: Success confirmation
Permissions: Owner or Admin
Note: Cannot remove the owner.
Promote a member to admin role.
Type: mutation
Input:
{
collectionId: string;
userId: string;
}Returns: Success confirmation
Permissions: Owner only
Demote an admin to member role.
Type: mutation
Input:
{
collectionId: string;
userId: string;
}Returns: Success confirmation
Permissions: Owner only
Transfer collection ownership to another user.
Type: mutation
Input:
{
collectionId: string;
newOwnerId: string;
}Returns: Success confirmation
Permissions: Owner only
Note: Previous owner becomes an admin after transfer.
Leave a collection (remove self).
Type: mutation
Input:
{
collectionId: string;
}Returns: Success confirmation
Note: Cannot leave if you're the owner. Transfer ownership first.
Step down from admin to member role.
Type: mutation
Input:
{
collectionId: string;
}Returns: Success confirmation
Manage API keys for programmatic access.
Create a new API key.
Type: mutation
Input:
{
name: string; // Descriptive name
mode: 'full_access' | 'collection_specific';
expiresIn?: number; // Seconds (optional, null = never expires)
collectionIds?: string[]; // Required if mode is collection_specific
}Returns:
{
id: string;
name: string;
key: string; // Full key (shown only once!)
prefix: string; // e.g., "dex_live_"
mode: string;
expiresAt: Date | null;
createdAt: Date;
}Important: The full key is only returned once. Store it securely!
List all API keys for the current user.
Type: query
Input: None
Returns:
Array<{
id: string;
name: string;
prefix: string;
start: string; // First few characters of key
mode: string;
enabled: boolean;
expiresAt: Date | null;
lastRequest: Date | null;
requestCount: number;
createdAt: Date;
}>;Note: Full keys are never returned after creation.
Delete an API key.
Type: mutation
Input:
{
keyId: string;
}Returns: Success confirmation
Grant an API key access to a specific collection.
Type: mutation
Input:
{
apiKeyId: string;
collectionId: string;
}Returns: Success confirmation
Note: Only applicable for collection_specific mode API keys.
Revoke an API key's access to a collection.
Type: mutation
Input:
{
apiKeyId: string;
collectionId: string;
}Returns: Success confirmation
Search and find users.
Search for users by name or email.
Type: query
Input:
{
query: string; // Search term (min 1 character)
}Returns:
Array<{
id: string;
name: string;
email: string;
image: string | null;
}>;Note: Limited to 10 results. Only searches active users if waitlist is enabled.
Fetch metadata and embedding information for URLs.
Get OpenGraph/oEmbed metadata for a URL.
Type: query (public, no auth required)
Input:
{
url: string; // Valid URL
}Returns:
{
title?: string;
description?: string;
image?: string;
favicon?: string;
author?: string;
siteName?: string;
}Check if a URL can be embedded in an iframe.
Type: query (public, no auth required)
Input:
{
url: string; // Valid URL
}Returns: boolean
Note: Checks X-Frame-Options and Content-Security-Policy headers.
DEX includes a Model Context Protocol (MCP) server for AI assistant integration.
Endpoint: POST /mcp
Authentication: API Key (X-API-Key header)
Protocol: JSON-RPC 2.0
Echo test for connection verification.
Input:
{
"message": "Hello"
}Returns:
{
"message": "Hello"
}List all collections accessible by the API key.
Input: None
Returns:
{
"collections": [
{
"id": "col_123",
"title": "Reading List",
"itemCount": 42
}
]
}Add multiple URLs to a collection.
Input:
{
"collectionId": "col_123",
"urls": ["https://example.com/1", "https://example.com/2"]
}Returns:
{
"added": 2,
"failed": 0,
"items": [...]
}Full-text search across items.
Input:
{
"query": "javascript",
"limit": 10
}Returns:
{
"results": [
{
"id": "item_123",
"title": "JavaScript Tutorial",
"url": "https://example.com/js",
"tldr": "Learn JavaScript basics..."
}
]
}Create a new collection.
Input:
{
"title": "New Reading List"
}Returns:
{
"id": "col_456",
"title": "New Reading List"
}Note: Only available for full_access mode API keys.
| Code | Description |
|---|---|
BAD_REQUEST |
Invalid input or malformed request |
UNAUTHORIZED |
Not authenticated |
FORBIDDEN |
Insufficient permissions |
NOT_FOUND |
Resource not found |
CONFLICT |
Resource already exists or conflict |
INTERNAL_SERVER_ERROR |
Server error |
{
error: {
code: 'UNAUTHORIZED',
message: 'You must be logged in to access this resource'
}
}401 Unauthorized:
- No valid session or API key
- API key expired or revoked
403 Forbidden:
- Insufficient permissions for collection
- Trying to perform owner-only action as admin/member
404 Not Found:
- Collection, item, or user doesn't exist
- User doesn't have access to resource
409 Conflict:
- Item already exists in collection
- Duplicate collection title (if enforced)
API Key Rate Limits:
- Default: 100 requests per minute
- Configurable per API key
- Uses token bucket algorithm with refill
Rate Limit Headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1634567890
Rate Limit Exceeded Response:
{
"error": {
"code": "TOO_MANY_REQUESTS",
"message": "Rate limit exceeded. Try again in 42 seconds."
}
}import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "@repo/server";
const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: "http://localhost:8787/api/trpc",
headers: {
"X-API-Key": "dex_live_your_key_here", // Or use session cookies
},
}),
],
});Query:
const collections = await trpc.collections.getUserCollections.query({
sortBy: "updatedAt",
order: "desc",
});Mutation:
const newCollection = await trpc.collections.create.mutate({
title: "My New Collection",
});import { trpc } from '~/lib/trpc';
function CollectionList() {
const { data, isLoading } = trpc.collections.getUserCollections.useQuery();
const createMutation = trpc.collections.create.useMutation();
const handleCreate = async () => {
await createMutation.mutateAsync({ title: 'New Collection' });
};
if (isLoading) return <div>Loading...</div>;
return <div>{/* Render collections */}</div>;
}- Use TypeScript: Take advantage of end-to-end type safety
- Handle Errors: Always catch and handle tRPC errors gracefully
- Batch Requests: tRPC batches queries automatically for performance
- Cache Wisely: Use TanStack Query for intelligent caching
- Secure API Keys: Never expose API keys in client-side code
- Check Permissions: Verify user has access before showing UI actions
- Validate Input: Use Zod schemas for runtime validation
For more details on the architecture and implementation, see ARCHITECTURE.md.