Skip to main content

lo-fi

Scroll down!
Subway-tolerant apps

A holistic local-first web toolkit

Build a schema

  • Define model fields, defaults, and query indexes
  • Schemas are TypeScript, so you can use familiar code and modules
  • Create migrations as your schema evolves
schema.ts
import {	collection,	schema} from '@lo-fi/web';import cuid from 'cuid';const posts = collection({	name: 'post',	primaryKey: 'id',	fields: {		id: {			type: 'string',			default: cuid,		},		title: {			type: 'string',		},		body: {			type: 'string',		},		createdAt: {			type: 'number',			default: Date.now,		},		image: {			type: 'file',			nullable: true,		}	},});export default schema({	version: 1,	collections: {		posts	}});

Generate a client

  • Generated code is type-safe to your schema
  • Queries and models are reactive
  • React hook bindings included
client.ts
import {	ClientDescriptor,	createHooks,	migrations} from './generated.js';export const clientDescriptor =	new ClientDescriptor({		migrations,		namespace: 'demo',	})export const hooks = createHooks();async function getAllPosts() {	const client = await		clientDescriptor.open();	const posts = await client.posts		.findAll().resolved;	return posts;}

Store local data

  • Store data in IndexedDB, no server or internet connection required
  • Undo/redo and optimistic updates out of the box
  • Assign files directly to model fields
app.tsx
import {	hooks,	clientDescriptor} from './client.js';export function App() {	return (		<Suspense>			<hooks.Provider				value={clientDescriptor}>				<Posts />			</hooks.Provider>		</Suspense>	);}function Posts() {	const posts = hooks.useAllPosts();	return (		<ul>			{posts.map((post) => (				<Post					key={post.get('id')}					post={post}				/>			))}		</ul>	);}function Post({ post }) {	const {		title,		body,		image	} = hooks.useWatch(post);	return (		<li>			<input				value={title}				onChange={(e) => post.set(					'title', e.target.value				)}			/>			<textarea				value={body}				onChange={(e) => post.set(					'body',					e.target.value				)}			/>			<input				type="file"				onChange={(e) => post.set(					'image',					e.target.files[0]				)}			/>			{image &&				<img src={image.url} />			}		</li>	);}

Sync with a server

  • Access data across devices
  • Share a library with collaborators
  • Peer presence data and profile info
  • Works realtime, pull-based, even switching dynamically
server.ts
import { Server } from '@lo-fi/server';const server = new Server({	databaseFile: 'db.sqlite',	tokenSecret: 'secret',	profiles: {		get: async (userId: string) => {			return {				id: userId,			}		}	}});server.listen(3000);