Setup & download
npm release coming. rifledb and @rifledb/server are not on the npm registry yet. The install commands below are the ones that will work once they are. Until then, both run from the rifleDB source workspace, where apps depend on "rifledb": "workspace:*".
Requirements
- Node.js
- 22 or newer. The Docker images run Node 24.
- Redis
- 7 or newer in production, with append-only persistence. In development the server can keep everything in memory instead.
- React
- 19 or newer, for
rifledb/reactandrifledb/next. The core client needs no React and runs in browsers and in Node.
Install the SDK
npm install rifledbpnpm add rifledbyarn add rifledbOne package with four entry points:
rifledb- The realtime client:
createRiflewith its cache, optimistic writes, offline queue and device keys, pluscreateHttpClientfor signed REST. rifledb/reactRifleProviderand the hooks:useRifle,useFolder,useAction,useEvent,usePresence,useIdentity,useStepUp,useConnection, andcreateRifleHooksfor typed paths.rifledb/nextRifleHydrate: seeds values fetched on the server, so the first paint matches and the hooks take over live.rifledb/next/servercreateRifleServer: server-only, signed REST with a service key, andsnapshotForfor hydration.
Connect an app
Both examples count clicks at playground/counter, a key of the sandbox folder the server ships with, so they work against a fresh development server. Open the page in two tabs and click: both counters move.
import { createRoot } from 'react-dom/client'
import { createRifle } from 'rifledb'
import { RifleProvider, useRifle } from 'rifledb/react'
const rifle = createRifle({ url: 'http://localhost:4000' })
function Counter() {
const [count, api] = useRifle('playground/counter')
return <button onClick={() => api.inc()}>Clicked {count ?? 0} times</button>
}
createRoot(document.getElementById('root')!).render(
<RifleProvider client={rifle}>
<Counter />
</RifleProvider>,
)Vite serves on localhost:5173, one of the origins a development server accepts.
'use client'
import type { ReactNode } from 'react'
import { createRifle } from 'rifledb'
import { RifleProvider } from 'rifledb/react'
// connect: false, because this module also runs during server rendering. The provider connects in the browser.
const rifle = createRifle({
url: process.env.NEXT_PUBLIC_RIFLE_URL ?? 'http://localhost:4000',
connect: false,
})
export function Providers({ children }: { children: ReactNode }) {
return <RifleProvider client={rifle}>{children}</RifleProvider>
}'use client'
import { useRifle } from 'rifledb/react'
export function Counter() {
const [count, api, { pending }] = useRifle('playground/counter')
return (
<button onClick={() => api.inc()} aria-busy={pending}>
Clicked {count ?? 0} times
</button>
)
}Wrap the content of app/layout.tsx in <Providers> and render <Counter /> on any page.
import { RifleHydrate } from 'rifledb/next'
import { createRifleServer } from 'rifledb/next/server'
import { Cards } from './cards'
const rifle = createRifleServer({
url: process.env.RIFLE_URL!,
serviceKey: process.env.RIFLE_SERVICE_KEY!,
})
export default async function BoardPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const title = await rifle.get(`boards/${id}/title`)
const snapshot = await rifle.snapshotFor([`boards/${id}/cards`])
return (
<main>
<h1>{String(title)}</h1>
<RifleHydrate snapshot={snapshot}>
<Cards boardId={id} />
</RifleHydrate>
</main>
)
}'use client'
import { useRifle } from 'rifledb/react'
type Card = { id: string; text: string }
export function Cards({ boardId }: { boardId: string }) {
const [cards, api, { pending }] = useRifle<Card[]>(`boards/${boardId}/cards`)
return (
<section aria-busy={pending}>
{(cards ?? []).map((card) => <article key={card.id}>{card.text}</article>)}
<button onClick={() => api.push({ id: crypto.randomUUID(), text: 'New card' })}>Add card</button>
</section>
)
}The first paint comes from the server over signed REST; the hooks keep it live. The service key stays on the server. Service keys explains how to mint one.
Run the server
The server is one Node.js process that serves REST, server-sent events and WebSocket on one port. @rifledb/server comes to npm together with the SDK; today it runs from the workspace.
Declare your folders
Folders live in server/rifle.config.tsx. It is JSX, but nothing renders: each element is plain data the server loads at boot. Add yours inside <Rifle>, next to the folders it ships with.
import { Folder, Rifle, Users } from '@rifledb/server'
export default (
<Rifle>
<Users registration='open' />
{/* … the folders it ships with: system (admin), playground, demo … */}
<Folder path='polls' mode='public' publicWrite />
<Folder path='boards' mode='private' />
<Folder path='rooms' mode='public' publicWrite fast={{ persist: false, ttlMs: 15_000 }} />
</Rifle>
)Modes, keys, grants, actions and fast folders: Access control.
Start it
pnpm install
pnpm -F @rifledb/server dev # http://localhost:4000, data in memory (gone on restart)
# with Redis, so data survives restarts (needs redis-server on your PATH)
pnpm redis:dev # Redis on port 6380, append-only file in .data/redis
REDIS_URL=redis://localhost:6380 pnpm -F @rifledb/server devFor production, build the bundle and start it with the settings of your domain:
pnpm -F @rifledb/server build # bundles server/dist/main.js
REDIS_URL=redis://localhost:6379 \
RIFLE_PUBLIC_ORIGIN=https://db.example.com \
RIFLE_ALLOWED_ORIGINS=https://app.example.com \
pnpm -F @rifledb/server start # node dist/main.jsThe settings that matter most:
| Variable | Example | What it does |
|---|---|---|
REDIS_URL | redis://redis:6379 | Redis to store everything in. Without it: the in-memory store. |
RIFLE_PUBLIC_ORIGIN | https://db.example.com | The origin clients reach the server at. Part of every socket sign-in. |
RIFLE_ALLOWED_ORIGINS | https://app.example.com | Browser origins that may connect. The default allows only the local development ports. |
RIFLE_WEBAUTHN | [{"rpID":"app.example.com", …}] | Passkey relying parties. Needed for step-up (secret folders, admin changes) on your own domain. |
RIFLE_TRUST_PROXY | 172.18.0.0/16 | The network of your reverse proxy, so rate limits see each visitor’s own address. |
RIFLE_PORT | 4000 | Listening port (default 4000). |
Every variable, rate limits, backups and metrics: Operations.
Docker Compose
deploy/Dockerfile.server builds the server image: Node 24, the bundled server, and a health check on /v1/_/health. This file runs it next to Redis. Save it in the workspace root:
# rifle.compose.yml, in the root of the rifleDB workspace
services:
redis:
image: redis:8-alpine
restart: unless-stopped
# append-only file, fsync every second
command: ['redis-server', '--appendonly', 'yes', '--appendfsync', 'everysec']
volumes:
- redis-data:/data
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 10s
timeout: 3s
retries: 5
server:
build:
context: .
dockerfile: deploy/Dockerfile.server
restart: unless-stopped
init: true
stop_grace_period: 20s # a clean shutdown flushes pending writes (up to 15 s)
depends_on:
redis:
condition: service_healthy
environment:
REDIS_URL: redis://redis:6379
RIFLE_PUBLIC_ORIGIN: https://db.example.com
RIFLE_ALLOWED_ORIGINS: https://app.example.com
RIFLE_WEBAUTHN: '[{"rpID":"app.example.com","rpName":"My app","origins":["https://app.example.com"]}]'
# RIFLE_TRUST_PROXY: 172.18.0.0/16 # the network your reverse proxy connects from
ports:
- '4000:4000'
volumes:
redis-data: {}docker compose -f rifle.compose.yml up -d --build
curl http://localhost:4000/v1/_/health # {"ok":true,…,"store":"redis","healthy":true,…}The server speaks plain HTTP: put a reverse proxy with TLS in front of it, and set RIFLE_TRUST_PROXY to the network the proxy connects from, so rate limits apply per visitor. The full production stack (the server, Redis, this site and the admin app behind nginx-proxy) is deploy/docker-compose.yml, with a step-by-step guide in deploy/README.md.
First admin
While no admin exists, the server prints a one-time setup token when it starts: valid 60 minutes, single use, a new one after each restart.
docker compose -f rifle.compose.yml logs server | grep -A 8 'rifleDB admin setup'Open the admin app’s sign-in page, choose “I have a setup token”, paste it and create the passkey the browser offers. The admin app is a Next.js app in the same workspace: pnpm -F @rifledb/admin dev serves it on localhost:3001, and deploy/Dockerfile.next builds it for production. Its origin must be in RIFLE_ALLOWED_ORIGINS and RIFLE_WEBAUTHN. Once your admin and passkey exist, set RIFLE_ADMIN_SETUP=off.