Quickstart
Install the SDK
npm install rifledbnpm release coming. Until rifledb is published, apps inside the rifleDB workspace depend on it with "rifledb": "workspace:*" instead. Setup has the details.
Run the server
The server is one Node.js process with Redis behind it. Start both from the workspace root:
# in the rifleDB workspace
pnpm install
pnpm redis:dev # Redis on port 6380 (another terminal)
REDIS_URL=redis://localhost:6380 pnpm -F @rifledb/server dev # rifleDB on http://localhost:4000
# without REDIS_URL the server keeps everything in memory (gone on restart)Folders are declared in server/rifle.config.tsx. It is JSX, but nothing renders: each element is plain data the server loads at boot. Add a public folder that anyone may write to, next to the folders it already has:
export default (
<Rifle>
<Users registration='open' />
{/* … the folders it ships with: system (admin), playground, demo … */}
<Folder path='polls' mode='public' publicWrite />
</Rifle>
)Create a client
createRifle opens one WebSocket to the server. With identity: 'auto' each browser gets an Ed25519 device key, kept in IndexedDB and non-extractable wherever the browser supports Ed25519 in WebCrypto. There are no passwords to store.
import { createRifle } from 'rifledb'
// connect: false keeps server rendering offline; <RifleProvider> connects it in the browser.
export const rifle = createRifle({ url: 'http://localhost:4000', identity: 'auto', connect: false })Wrap your app once with the provider. It opens the socket in the browser, never during server rendering:
'use client'
import type { ReactNode } from 'react'
import { RifleProvider } from 'rifledb/react'
import { rifle } from '@/lib/rifle'
export function Providers({ children }: { children: ReactNode }) {
return <RifleProvider client={rifle}>{children}</RifleProvider>
}Read and write with useRifle
useRifle(path) subscribes to a path and returns [value, api, { rev, pending, status }]. Writes show up in your tab at once and are rolled back if the server refuses them.
'use client'
import { useRifle } from 'rifledb/react'
export function Votes() {
const [votes, api, { pending }] = useRifle('polls/launch/votes')
return (
<button onClick={() => api.inc()}>
{votes ?? 0} votes{pending ? ' (saving)' : ''}
</button>
)
}Open the page in two windows and click. Both counters move together.
Same data over REST
Every path is also a URL under /v1/. Reads return an ETag; send it back as If-Match to write only if nothing changed in between. Errors come back as application/problem+json.
curl http://localhost:4000/v1/polls/launch/votes
# {"v":2,"r":7}, with ETag: "<epoch>:<rev>"
curl -X PATCH http://localhost:4000/v1/polls/launch/votes \
-H 'content-type: application/rifle-op+json' \
-d '{"op":"inc","args":{"by":5}}'
curl -N 'http://localhost:4000/v1/polls/launch/votes?stream=1'
# every change, as server-sent eventsYour first private folder
Every user gets a private folder at users/<uid>. By default only that user’s devices can read or write notes in it.
await rifle.identity.ensure()
const uid = rifle.identity.uid
await rifle.set(`users/${uid}/notes/first`, 'Only my devices can read this.')Anyone else is turned away, including a curl request without a signature:
curl -i http://localhost:4000/v1/users/u_8fQ2xV/notes/first
# HTTP/1.1 401 Unauthorized
# content-type: application/problem+jsonTo work on a folder with other people, declare it private, for example <Folder path='boards' mode='private' />, and give access with grants, folder keys or claim invites: Access control explains how.