Skip to content

Database Quick Start

Varity Team Core Contributors Updated March 2026

Varity includes a zero-config database. Import db from the SDK and start querying immediately — no database setup, no connection strings, no ORM configuration.

The database module is included in @varity-labs/sdk. No additional packages needed.

Terminal window
npm install @varity-labs/sdk

Start by defining TypeScript interfaces for your data:

src/types/index.ts
export interface Project {
id?: string;
name: string;
description: string;
status: 'active' | 'paused' | 'completed';
owner: string;
members: string[];
dueDate: string;
createdAt: string;
}
export interface Task {
id?: string;
projectId: string;
title: string;
description?: string;
status: 'todo' | 'in_progress' | 'done';
priority: 'low' | 'medium' | 'high';
assignee?: string;
dueDate?: string;
createdAt: string;
}

Create a file that exports typed collection accessors:

src/lib/database.ts
import { db } from '@varity-labs/sdk';
import type { Project, Task } from '../types';
export const projects = () => db.collection<Project>('projects');
export const tasks = () => db.collection<Task>('tasks');

Each call to db.collection<T>('name') returns a typed collection with full CRUD operations.

import { projects } from './database';
// Insert a single document
const newProject = await projects().add({
name: 'My Project',
description: 'A new project',
status: 'active',
owner: 'user@example.com',
members: ['user@example.com'],
dueDate: '2026-03-01',
createdAt: new Date().toISOString(),
});
// Expected output:
// {
// id: "550e8400-e29b-41d4-a716-446655440000", // Auto-generated ID
// name: "My Project",
// description: "A new project",
// status: "active",
// owner: "user@example.com",
// members: ["user@example.com"],
// dueDate: "2026-03-01",
// createdAt: "2026-03-05T14:23:11.000Z"
// }
// Get all documents in a collection
const allProjects = await projects().get();
// Returns: Array of all project documents
// Get with pagination (useful for large datasets)
const page1 = await projects().get({ limit: 10, offset: 0 }); // First 10 items
const page2 = await projects().get({ limit: 10, offset: 10 }); // Next 10 items
// Get with ordering (prefix with - for descending)
const newest = await projects().get({ orderBy: '-createdAt' });
// Returns: Projects sorted by creation date, newest first

Filtering: The .get() method returns all documents. Filter on the client side:

// Fetch all projects first
const allProjects = await projects().get();
// Filter by status
const activeProjects = allProjects.filter(p => p.status === 'active');
// Returns: Only projects with status === 'active'
// Filter tasks by project ID (relational filtering)
const projectTasks = (await tasks().get()).filter(t => t.projectId === 'proj-123');
// Returns: Only tasks belonging to project 'proj-123'
// Update by ID
await projects().update('proj-123', {
status: 'completed',
});
// Update specific fields (partial update)
await projects().update('proj-123', {
name: 'Updated Name',
description: 'New description',
});
// Delete by ID
await projects().delete('proj-123');

The recommended pattern is to wrap database operations in a custom React hook. This provides loading states, error handling, and optimistic UI updates:

src/lib/hooks.ts
'use client';
import { useState, useEffect, useCallback } from 'react';
import { projects } from './database';
import type { Project } from '../types';
export function useProjects() {
const [data, setData] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Load all projects on mount
const refresh = useCallback(async () => {
try {
setLoading(true);
setError(null);
const result = await projects().get();
setData(result as Project[]);
} catch (err) {
// Proper error handling with fallback message
setError(err instanceof Error ? err.message : 'Failed to load');
console.error('Failed to load projects:', err);
} finally {
// Always set loading to false, even on error
setLoading(false);
}
}, []);
useEffect(() => { refresh(); }, [refresh]);
// Create with optimistic UI (instantly show new item before server confirms)
const create = async (input: Omit<Project, 'id' | 'createdAt'>) => {
// Generate temporary ID for optimistic update
const optimistic: Project = {
...input,
id: `temp-${Date.now()}`,
createdAt: new Date().toISOString(),
};
// Add to UI immediately
setData(prev => [optimistic, ...prev]);
try {
// Save to database
await projects().add({ ...input, createdAt: optimistic.createdAt });
// Refresh to get real ID from server
await refresh();
} catch (err) {
// Rollback on failure - remove the optimistic item
setData(prev => prev.filter(p => p.id !== optimistic.id));
console.error('Failed to create project:', err);
throw err; // Re-throw so UI can show error
}
};
// Update with optimistic UI
const update = async (id: string, updates: Partial<Project>) => {
const original = data.find(p => p.id === id);
setData(prev => prev.map(p => p.id === id ? { ...p, ...updates } : p));
try {
await projects().update(id, updates);
} catch (err) {
if (original) setData(prev => prev.map(p => p.id === id ? original : p));
throw err;
}
};
// Delete with optimistic UI
const remove = async (id: string) => {
const original = data.find(p => p.id === id);
setData(prev => prev.filter(p => p.id !== id));
try {
await projects().delete(id);
} catch (err) {
if (original) setData(prev => [...prev, original]);
throw err;
}
};
return { data, loading, error, create, update, remove, refresh };
}
src/app/dashboard/projects/page.tsx
'use client';
import { useProjects } from '../../../lib/hooks';
export default function ProjectsPage() {
const { data: projects, loading, error, create, remove } = useProjects();
if (loading) return <p>Loading projects...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<h1>Projects ({projects.length})</h1>
<button onClick={() => create({
name: 'New Project',
description: 'Created just now',
status: 'active',
owner: 'me@example.com',
members: ['me@example.com'],
dueDate: '2026-12-31',
})}>
Add Project
</button>
{projects.map(project => (
<div key={project.id}>
<h3>{project.name}</h3>
<p>{project.description}</p>
<span>Status: {project.status}</span>
<button onClick={() => remove(project.id!)}>Delete</button>
</div>
))}
</div>
);
}

To filter documents based on a parent relationship (e.g., tasks for a specific project):

src/lib/hooks.ts
export function useTasks(projectId?: string) {
const [allTasks, setAllTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const refresh = useCallback(async () => {
try {
setLoading(true);
const result = await tasks().get();
setAllTasks(result as Task[]);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { refresh(); }, [refresh]);
// Client-side filtering
const data = projectId
? allTasks.filter(t => t.projectId === projectId)
: allTasks;
// ... create, update, remove (same pattern as above)
return { data, loading, error, refresh };
}
.env.local
NEXT_PUBLIC_VARITY_APP_ID=your-app-id
NEXT_PUBLIC_VARITY_APP_TOKEN=your-app-token
NEXT_PUBLIC_VARITY_DB_PROXY_URL=your-db-url

Every feature in a Varity app follows the same 3-step pattern:

  1. Type — Define a TypeScript interface in types/index.ts
  2. Collection — Create a typed collection accessor in database.ts
  3. Hook — Build a React hook with CRUD + optimistic updates
  4. Page — Use the hook in a page component

This pattern is used throughout the SaaS Starter Template for projects, tasks, and team members.