Add a Feature
Add a Feature
Every feature in Warpkit follows the same structure. Here's how to scaffold one from scratch.
Feature structure
src/features/my-feature/
├── index.ts # barrel: public API
├── server/
│ ├── my-feature.queries.ts # read-only server functions (GET)
│ ├── my-feature.mutations.ts # write server functions (POST)
│ ├── my-feature.rules.ts # pure predicates shared between queries/mutations (optional)
│ └── my-feature-crud.test.ts # integration tests
└── components/ # optional UI
├── MyWidget.tsx
└── index.ts
Step 1: Schema
Add your table to src/lib/db/user-migrations.ts (append only):
{
id: '001_add_my_feature',
run: db => {
db.run(`
CREATE TABLE IF NOT EXISTS my_items (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`);
},
},
Step 2: Server functions
Create src/features/my-feature/server/my-feature.queries.ts:
import { err, ok } from '@bitclaw/result';
import { createServerFn } from '@tanstack/react-start';
import { ERROR_CODES } from '@/lib/constants';
import { getUserDb } from '@/lib/db/user-db';
import { requireUser } from '@/server/require-user';
export const getItems = createServerFn({ method: 'GET' }).handler(async () => {
const user = await requireUser();
if (!user) return err(ERROR_CODES.UNAUTHORIZED, 'Not authenticated');
const db = getUserDb(user.id);
const items = db
.query<{ id: string; title: string }, []>(
'SELECT * FROM my_items ORDER BY created_at DESC'
)
.all();
return ok(items);
});
Create src/features/my-feature/server/my-feature.mutations.ts:
import { randomUUIDv7 } from 'bun';
import { err, ok } from '@bitclaw/result';
import { createServerFn } from '@tanstack/react-start';
import { z } from 'zod';
import { ERROR_CODES } from '@/lib/constants';
import { getUserDb, withWriteLock } from '@/lib/db/user-db';
import { logUserEvent } from '@/lib/db/user-events';
import { requireUser } from '@/server/require-user';
export const createItem = createServerFn({ method: 'POST' })
.inputValidator(z.object({ title: z.string().min(1).max(200) }))
.handler(async ({ data }) => {
const user = await requireUser();
if (!user) return err(ERROR_CODES.UNAUTHORIZED, 'Not authenticated');
return withWriteLock(user.id, () => {
const db = getUserDb(user.id);
const id = randomUUIDv7();
db.run(
'INSERT INTO my_items (id, title, created_at) VALUES (?, ?, ?)',
[id, data.title, Date.now()]
);
logUserEvent(db, 'item.created', { id });
return ok({ id, title: data.title });
});
});
Step 3: Tests
Create src/features/my-feature/server/my-feature-crud.test.ts:
import { describe, expect, test } from 'bun:test';
import { makeTestDb } from '@/test/db';
describe('my-feature', () => {
test('inserts and reads items', () => {
const db = makeTestDb();
db.run("INSERT INTO my_items (id, title, created_at) VALUES ('1', 'Test', 0)");
const row = db.query<{ title: string }, []>('SELECT title FROM my_items').get();
expect(row?.title).toBe('Test');
});
});
Run: bun test src/features/my-feature
Step 4: Barrel
Create src/features/my-feature/index.ts:
export { getItems } from './server/my-feature.queries';
export { createItem } from './server/my-feature.mutations';
Step 5: Route
Create src/routes/_app.dashboard.my-feature.tsx:
import { createFileRoute } from '@tanstack/react-router';
import { getItems } from '@/features/my-feature';
export const Route = createFileRoute('/_app/dashboard/my-feature')({
component: MyFeaturePage,
loader: () => getItems(),
});
function MyFeaturePage() {
const result = Route.useLoaderData();
return (
<ul>
{result.data?.map(item => <li key={item.id}>{item.title}</li>)}
</ul>
);
}
Step 6: Generate route tree
bun run generate
bun run dev
Visit /dashboard/my-feature.