silosdk
v0.0.10
Published
Silo SDK provides local-first persistence primitives for Expo apps. It owns local storage and returns TanStack-compatible option objects instead of wrapping TanStack hooks.
Readme
Silo SDK
Silo SDK provides local-first persistence primitives for Expo apps. It owns local storage and returns TanStack-compatible option objects instead of wrapping TanStack hooks.
Silo owns:
- local platform persistence
- flat primitive document constraints
- defaults-first source shape
- settings storage
- media file ownership and resolution
- ID generation through
expo-crypto
TanStack owns:
createCollection()and live collection queriesuseQuery()anduseMutation()- query and mutation state
Install
yarn add silosdk @tanstack/react-db @tanstack/react-query expo-crypto expo-sqlite expo-file-systemNative apps use Expo SQLite, Expo FileSystem, and Expo SQLite KV storage. Web builds use IndexedDB for stores, settings, and media blobs through browser export entrypoints, so the web bundle does not need to load the native SQLite or filesystem storage modules.
Stores
Stores define locally persisted document collections and a link collection for relationships between them. Native storage uses SQLite; web storage uses IndexedDB. Stores produce options for TanStack DB collections.
import { createCollection } from '@tanstack/react-db'
import { createID, createStore } from 'silosdk/store'
type Post = {
title: string
body: string
published: boolean
coverImage: string | null
}
type Tag = {
name: string
}
export const store = createStore<{
posts: Post
tags: Tag
}>({
posts: {
title: '',
body: '',
published: false,
coverImage: null,
},
tags: {
name: '',
},
})
export const posts = createCollection(store.collectionOptions('posts'))
export const tags = createCollection(store.collectionOptions('tags'))
export const links = createCollection(store.linkCollectionOptions())Query and mutate with TanStack DB directly:
import { eq, useLiveQuery } from '@tanstack/react-db'
const { data: docs = [] } = useLiveQuery((q) =>
q.from({ posts }).where(({ posts }) => eq(posts.published, true)),
)
const doc = docs[0]
const post = doc?.data()
doc?.id
post?.titleconst id = createID()
const tx = posts.insert({
id,
data: {
title: 'Hello',
body: '',
published: false,
coverImage: null,
},
})
await tx.isPersisted.promise
await posts.update(id, (doc) => {
doc.set({ published: true })
}).isPersisted.promise
await posts.delete(id).isPersisted.promiseSource rows are intentionally flat. Field values must be:
type FieldValue = string | number | boolean | nullCreate links with the link collection utilities:
await links.utils.link('posts', postId, 'tags', tagId)
await links.utils.unlink('posts', postId, 'tags', tagId)
const isLinked = links.utils.has('posts', postId, 'tags', tagId)Link storage is flat and directionless. Collection names are sorted lexicographically and IDs are stored in the same order. Query rows expose nested paths for each collection pair:
import { eq, useLiveQuery } from '@tanstack/react-db'
const { data: taggedPosts = [] } = useLiveQuery((q) =>
q
.from({ posts })
.join({ links }, ({ posts, links }) =>
eq(posts.id, links.tags.posts.id),
)
.join({ tags }, ({ links, tags }) =>
eq(links.posts.tags.id, tags.id),
),
)For a posts to tags link:
links.tags.posts.id // post id
links.posts.tags.id // tag idSame-collection links are not supported. Define another collection when you need a different semantic relationship.
Sources
The lower-level source() API remains available from silosdk/store when you
only need one collection definition.
Settings
Settings are small persisted primitive values. Native storage uses
expo-sqlite/kv-store; web storage uses IndexedDB. Settings produce TanStack
Query options.
import { setting } from 'silosdk/settings'
export const theme = setting<'system' | 'light' | 'dark'>('theme', 'system')
export const reduceMotion = setting('reduceMotion', false)import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
const queryClient = useQueryClient()
const { data: value } = useQuery(theme.queryOptions())
const setTheme = useMutation(theme.mutationOptions({ queryClient }))
setTheme.mutate('dark')
setTheme.mutate((current) => (current === 'dark' ? 'light' : 'dark'))Setting values must also be flat primitives:
type SettingValue = string | number | boolean | nullMedia
Media is singleton infrastructure for app-owned files. Domain documents store stable media refs as plain strings, keeping source rows flat.
Native media imports copy files into Silo-owned document storage and persist the
mapping in SQLite. Web media imports store metadata and blobs in IndexedDB and
resolve refs to cached blob: URLs.
import { media, type MediaRef } from 'silosdk/media'Import a file with TanStack Query mutation options:
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
const queryClient = useQueryClient()
const importMedia = useMutation(media.mutationOptions({ queryClient }))
const coverImage = await importMedia.mutateAsync({
uri: pickedFile.uri,
kind: 'image',
name: pickedFile.name,
mimeType: pickedFile.type,
})Store the returned media://... ref in source rows:
posts.update(postId, (draft) => {
draft.set({ coverImage })
})Resolve a media ref to a local file URI:
const { data: uri } = useQuery(media.queryOptions(post.coverImage))media.queryOptions() returns string | null. It returns null when the ref is
empty, invalid, unknown, or missing on disk.
Delete a media ref when your app no longer stores it:
const deleteMedia = useMutation(media.deleteMutationOptions({ queryClient }))
await deleteMedia.mutateAsync(coverImage)Deletion removes Silo's copied file and media mapping only. Remove the ref from
your own source rows or settings separately. Invalid or unknown refs are safe and
return false.
API Shape
Silo exposes definition objects and option factories:
createCollection(store.collectionOptions('posts'))
createCollection(store.linkCollectionOptions())
useQuery(theme.queryOptions())
useMutation(theme.mutationOptions({ queryClient }))
useQuery(media.queryOptions(ref))
useMutation(media.mutationOptions({ queryClient }))
useMutation(media.deleteMutationOptions({ queryClient }))There is no Silo provider and no Silo hook wrapper API.
