Quick Start
Get up and running with Gluonic in 5 minutes. Build a simple task app with offline support and real-time sync.
What You’ll Build
A task list app with:
- âś… Synchronous data access (no loading states!)
- âś… Offline support (works without network)
- âś… Real-time sync (instant updates)
- âś… Optimistic UI (instant feedback)
Prerequisites
- Node.js 18+
- PostgreSQL (or any supported database)
- Basic knowledge of React and TypeScript
Step 1: Install Gluonic
# Server packages
pnpm add @gluonic/server @gluonic/server-prisma @gluonic/auth-jwt
# Client packages
pnpm add @gluonic/client @gluonic/client-drizzleStep 2: Server Setup
Create a sync server:
import { SyncServer } from '@gluonic/server'
import { PrismaAdapter } from '@gluonic/server-prisma'
import { JWTAuth } from '@gluonic/auth-jwt'
import { prisma } from './db'
// Create database adapter (auto-discovers models from Prisma schema)
const database = PrismaAdapter({ prisma })
const server = SyncServer({
database,
auth: JWTAuth({ secret: process.env.JWT_SECRET })
})
await server.listen({ port: 3000 })Note: Gluonic works with any database through adapters. See Server Setup for other database options.
Step 3: Define Models
import { Model, ClientModel, Property, ManyToOne, Collection } from '@gluonic/client'
@ClientModel('user')
export class User extends Model {
@Property()
id = crypto.randomUUID()
@Property()
name!: string
@Property()
email!: string
}
@ClientModel('task')
export class Task extends Model {
@Property()
id = crypto.randomUUID()
@Property()
title!: string
@Property()
done = false
@Property()
assigneeId?: string
@ManyToOne('user', 'assigneeId')
assignee?: User
}Notice:
- IDs use initializers (auto-generated, type inferred)
- Required fields use
!(name, email, title) - Optional fields use
?(assigneeId) - Clean relationship types (User, not LazyReference)
Step 4: Client Setup
Create a sync client in your React app:
import { SyncClient } from '@gluonic/client'
import { DrizzleAdapter } from '@gluonic/client-drizzle'
import { db } from './db'
import { Task, User } from './models'
// Create storage adapter (local database for offline support)
const storage = DrizzleAdapter({ db })
export const client = SyncClient({
server: 'http://localhost:3000/sync/v1',
storage,
models: [Task, User]
})Note: Drizzle adapter is currently the supported client storage adapter. See Client Setup for configuration details.
Step 5: Wrap Your App
import { SyncProvider } from '@gluonic/client'
import { client } from './sync'
import { useAuth } from './auth'
function App() {
const { token } = useAuth()
useEffect(() => {
client.store.init()
}, [])
return (
<SyncProvider client={client} token={token}>
<TaskList />
</SyncProvider>
)
}Note: When
tokenprop changes, SyncProvider automatically re-syncs. Whentokenisnull, sync is paused.
Step 6: Use in Components
OOP Creation Style (v1.1+)
Create and manipulate models as instances:
import { observer, useCollectionModels } from '@gluonic/client'
import { Task } from './models'
const TaskList = observer(() => {
const tasks = useCollectionModels<Task>('task')
const addTask = async () => {
const task = new Task({ title: 'New task' })
await task.save() // ID auto-generated
}
const toggleTask = async (task: Task) => {
task.done = !task.done
await task.save() // updatedAt auto-updated
}
return (
<div>
<button onClick={addTask}>Add Task</button>
{tasks.map(task => (
<div key={task.id}>
<input
type='checkbox'
checked={task.done}
onChange={() => toggleTask(task)}
/>
<span>{task.title}</span>
<span className='text-sm'>
{task.assignee?.value?.name ?? 'Unassigned'}
</span>
</div>
))}
</div>
)
})v1.1+ Feature: OOP pattern shown above.
Current: Use functional style below.
Functional Creation Style (Current)
Create through store methods:
import { observer, useCollectionModels, useStore } from '@gluonic/client'
import { Task } from './models'
const TaskList = observer(() => {
const tasks = useCollectionModels<Task>('task')
const store = useStore()
const addTask = async () => {
const id = crypto.randomUUID()
await store.create('task', id, {
title: 'New task',
done: false
})
}
const toggleTask = async (task: Task) => {
await store.save('task', task.id, { done: !task.done })
}
return (
<div>
<button onClick={addTask}>Add Task</button>
{tasks.map(task => (
<div key={task.id}>
<input
type='checkbox'
checked={task.done}
onChange={() => toggleTask(task)}
/>
<span>{task.title}</span>
<span className='text-sm'>
{task.assignee?.value?.name ?? 'Unassigned'}
</span>
</div>
))}
</div>
)
})Data Access Patterns
Gluonic provides two patterns for handling loading states:
Pattern 1: Progressive Enhancement (Recommended)
Handle loading inline - data appears as it loads:
import { observer, useCollectionModels, useStore } from '@gluonic/client'
import { Task } from './models'
const TaskList = observer(() => {
const tasks = useCollectionModels<Task>('task')
const store = useStore()
const addTask = async () => {
const id = crypto.randomUUID()
await store.create('task', id, {
title: 'New task',
done: false
})
}
const toggleTask = async (task: Task) => {
await store.save('task', task.id, { done: !task.done })
}
return (
<div>
<button onClick={addTask}>Add Task</button>
{tasks.length === 0 ? (
<div className='opacity-50'>No tasks yet</div>
) : (
tasks.map(task => (
<div key={task.id}>
<input
type='checkbox'
checked={task.done}
onChange={() => toggleTask(task)}
/>
<span>{task.title}</span>
{/* Optional chaining - assignee may be undefined initially */}
<span className='text-sm'>
{task.assignee.value?.name ?? 'Unassigned'}
</span>
</div>
))
)}
</div>
)
})How it works:
- First render:
tasksis[], assignees areundefined - Background: Data loads from storage/network
- MobX: Re-renders when data arrives
- Second render: Shows loaded data
Pattern 2: Guaranteed Data with Suspense
Use Suspense for cleaner code when you need guaranteed data:
import { Suspense } from 'react'
import { observer, useCollectionModels } from '@gluonic/client'
import { Task } from './models'
export default function TaskListWrapper() {
return (
<Suspense fallback={<div>Loading tasks...</div>}>
<TaskList />
</Suspense>
)
}
const TaskList = observer(() => {
const tasks = useCollectionModels<Task>('task')
const store = useStore()
const addTask = async () => {
const id = crypto.randomUUID()
await store.create('task', id, {
title: 'New task',
done: false
})
}
const toggleTask = async (task: Task) => {
await store.save('task', task.id, { done: !task.done })
}
return (
<div>
<button onClick={addTask}>Add Task</button>
{tasks.map(task => (
<div key={task.id}>
<input
type='checkbox'
checked={task.done}
onChange={() => toggleTask(task)}
/>
<span>{task.title}</span>
{/* No optional chaining - guaranteed loaded with .suspense */}
<span className='text-sm'>
{task.assignee.suspense.name}
</span>
</div>
))}
</div>
)
})How it works:
- Suspense boundary shows spinner while loading
- Component only renders when all data ready
- Inside: Use
.suspenseaccessors for guaranteed data - No optional chaining needed
🎉 That’s It!
You now have a working sync app! Notice:
- âś… No
useStatefor tasks - âś… No
useEffectto load data - âś… No manual loading states
- âś… No async/await in render logic
- âś… Works offline automatically
- âś… Real-time updates when online
Next Steps
Choose your learning path:
Learn the Concepts
- Synchronous API - How Gluonic feels synchronous
- Core Concepts - All fundamental ideas
- Architecture - How it all works
Configure for Production
- Server Setup - Advanced server configuration
- Client Setup - Advanced client features
- Authentication - Secure your endpoints
See Examples
- Blog App Example - Complete working app
- API Reference - Complete API documentation