The reactive backend for agents and apps.
Your app shouldn't poll for answers. Answers should flow to your app.
Wavelet lets you define a computation in SQL and subscribe to its result. When the underlying data changes, every connected app and AI agent receives the updated result automatically. No API to build, no cache to manage, no WebSocket to wire up.
// Define what to compute
queries: {
revenue: {
query: sql`SELECT tenant_id, SUM(amount) AS total FROM orders GROUP BY tenant_id`,
filterBy: 'tenant_id',
}
}
// Subscribe from your app
const { data } = useWavelet('revenue')
// data updates automatically. Each tenant sees only their own numbers.Built on RisingWave. By the RisingWave team.
npx skills add risingwavelabs/skills --skill waveletOr read SKILL.md directly.
Your customers log in and see their own metrics updating live -- revenue, active users, API usage. One query definition, thousands of tenants, each isolated by JWT. Replaces a polling endpoint + cache + per-tenant auth check.
Stream API calls, aggregate tokens and cost per customer, push to both customer-facing dashboards and rate limiters. Same source of truth, no sync issues.
AI agents subscribe to computed queries via MCP and act autonomously when conditions change. An agent watches sla_violations -- rows appear when an order exceeds its SLA, disappear when resolved. The agent escalates, notifies, or triggers a remediation. No polling, no cron -- the agent reacts to computed state, not raw events.
Install RisingWave and Wavelet:
curl -L https://risingwave.com/sh | sh # install RisingWave
npm install @risingwave/wavelet # install Waveletwavelet dev auto-starts RisingWave if the binary or Docker is available.
1. Define your config
// wavelet.config.ts
import { defineConfig, sql } from '@risingwave/wavelet'
export default defineConfig({
database: 'postgres://root@localhost:4566/dev',
events: {
game_events: {
columns: {
player_id: 'string',
score: 'int',
event_type: 'string',
}
}
},
queries: {
leaderboard: sql`
SELECT player_id, SUM(score) AS total_score, COUNT(*) AS games_played
FROM game_events
GROUP BY player_id
ORDER BY total_score DESC
LIMIT 100
`,
},
})2. Start dev server
npx wavelet dev3. Try the example app
npm run build
npx vite --open /examples/sdk-leaderboard/See examples/sdk-leaderboard for a working browser demo, or examples/react-leaderboard for a React version.
4. Subscribe from your own app
npx wavelet generate # generates .wavelet/client.ts with full typesimport { TypedWaveletClient } from './.wavelet/client'
const wavelet = new TypedWaveletClient({ url: 'http://localhost:8080' })
// read current state
const rows = await wavelet.queries.leaderboard.get()
// subscribe to live updates
wavelet.queries.leaderboard.subscribe({
onData: (diff) => {
console.log(diff.inserted, diff.updated, diff.deleted)
}
})
// write events
await wavelet.events.game_events.emit({
player_id: 'alice',
score: 42,
event_type: 'win',
})AI agents query and write events as tool calls.
{
"mcpServers": {
"wavelet": {
"command": "npx",
"args": ["@risingwave/wavelet-mcp"],
"env": {
"WAVELET_DATABASE_URL": "postgres://root@localhost:4566/dev"
}
}
}
}| Tool | Description |
|---|---|
list_queries |
List all queries (materialized views) with schemas |
query |
Query a materialized view with optional filters |
list_events |
List all event tables |
emit_event |
Write an event |
emit_batch |
Write a batch of events |
run_sql |
Execute a read-only SQL query |
App / Agent <- WebSocket <- Wavelet Server <- SQL cursor <- RisingWave
| |
JWT filtering Incremental
+ fan-out computation
Write path. POST /v1/events/{name} inserts directly into RisingWave. No queue, no buffer. 200 means the row is persisted. RisingWave recomputes affected materialized views on its next barrier cycle (~1s by default), and Wavelet pushes the diff to subscribers. End-to-end latency from write to client update is typically 1-2 seconds.
Stateless server. Wavelet holds no persistent state. Cursor positions are in memory. On restart, cursors recover from RisingWave's subscription retention window (default 24h). During recovery, clients may receive duplicate diffs -- applications should handle updates idempotently (e.g. key by primary key, not append).
Single cursor per query. One subscription cursor feeds all connected clients. 1 client or 10,000 -- same RisingWave load.
Config-driven DDL. wavelet.config.ts is the source of truth. wavelet dev and wavelet push diff config against RisingWave and apply minimal changes (create/drop tables, materialized views, subscriptions).
JWT-scoped delivery. Queries with filterBy match the column value against a JWT claim. Filtering is enforced server-side -- clients cannot override it. Queries without filterBy broadcast all rows to all clients. For multi-tenant applications, omitting filterBy on a tenant-scoped query is a data leak -- Wavelet does not enforce this automatically.
Failure modes. If RisingWave goes down, cursor fetch returns an error and Wavelet retries after 1 second. Clients stay connected but receive no diffs until RisingWave recovers. If a WebSocket disconnects, the SDK reconnects with exponential backoff (1s to 30s) and resumes from the last cursor position. Each query has its own cursor and connection -- a slow query does not block other queries.
wavelet init # Create wavelet.config.ts
wavelet dev # Sync config + start dev server
wavelet push # Sync config to RisingWave (no server)
wavelet generate # Generate typed client at .wavelet/client.ts
wavelet status # Show current config summaryAll commands are idempotent. Supports --json for structured output.
GET /v1/health -> { status: "ok" }
GET /v1/queries -> list all queries
GET /v1/queries/{name} -> current rows
GET /v1/queries/{name}?key=value -> filtered rows
GET /v1/events -> list all events
POST /v1/events/{name} -> write single event
POST /v1/events/{name}/batch -> write batch of events
WS /subscribe/{name} -> real-time diffs
Apache 2.0. See LICENSE.