Local Development
Daily workflows, debugging and conventions for building with Init locally
Daily workflows, debugging, and conventions for building with Init.
Development Workflow
Starting Your Session
-
Start the database (local Supabase; points
.env.localat this branch's database):pnpm db:startEach git branch has its own database. On a branch you haven't used before it starts empty:
pnpm db:push && pnpm db:seed. -
Start dev servers:
pnpm dev -
Open your tools:
- Web app: http://localhost:3000
- Drizzle Studio:
pnpm -F db studio - Mobile: Expo Go or a simulator
Daily Commands
# Start development
pnpm dev
# Run one app
pnpm dev:web # Web only
pnpm dev:mobile # Mobile only
pnpm dev:extension # Extension only
pnpm dev:desktop # Desktop shell, plus the web app it opens
# Database
pnpm db:reset # Reset local database
pnpm db:push # Push schema changes
# Code quality
pnpm lint # oxlint
pnpm typecheck # TypeScript, and clippy for the desktop app
pnpm format # oxfmt, and cargo fmt
pnpm test # node:test, and cargo testMonorepo Structure
apps/
├── web/ # Next.js web app
├── mobile/ # Expo mobile app
├── extension/ # Chrome extension (WXT)
└── desktop/ # Tauri desktop app
packages/
├── permissions/ # Client-safe auth helpers: role permissions, slugify
├── contract/ # oRPC contract shared by server and clients
├── service/ # oRPC implementation + better-auth
├── db/ # Drizzle schema + local Supabase config
└── ui/ # Shared React componentsEvery client that calls the API - web and mobile - types its calls against contract and checks roles with permissions; the extension and the desktop shell open the web app itself. Only the server (web's route handlers and server components) imports service and db. Write business logic once, in service.
Working with the Database
Schema Changes
- Modify schema in
packages/db/src/drizzle-schema.ts - Push changes:
pnpm db:push - Verify in Drizzle Studio (
pnpm -F db studio)
No migration files - drizzle-kit push diffs the schema straight against the database.
Adding a Table
// packages/db/src/drizzle-schema.ts
export const post = pgTable.withRLS("post", (t) => ({
id: t.uuid().notNull().primaryKey().defaultRandom(),
organizationId: t
.text()
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
title: t.text().notNull(),
createdAt: t.timestamp({ withTimezone: true }).notNull().defaultNow(),
}));Always pgTable.withRLS - authorization lives in oRPC, and deny-by-default RLS means a non-owner connection reads nothing.
API Development
Adding Routes
Declare the procedure in packages/contract/src/<feature>/ (Zod schema + contract), then implement it in packages/service/src/<feature>/:
// packages/contract/src/post/post-contract.ts
export const postContract = {
list: organizationBase
.meta(openapi({ summary: "List posts", description: "...", tags: ["Post"] }))
.input(organizationInput)
.output(z.object({ posts: z.array(createSelectSchema(post)) })),
};
// packages/service/src/post/post-router.ts
const authed = os.post.use(requireSession);
export const postRouter = {
list: authed.list.use(requireOrganization).handler(async ({ context }) => {
const posts = await context.db.query.post.findMany({
where: { organizationId: context.organization.id },
});
return { posts };
}),
};requireSession and requireOrganization (from ../orpc) prove the session and membership before the handler runs. Register the contract in packages/contract/src/index.ts and the router in packages/service/src/root-router.ts. See the API guide for the full pattern.
Using Routes in Components
import { useSuspenseQuery } from "@tanstack/react-query";
import { orpc } from "@/orpc/react";
const { data } = useSuspenseQuery(orpc.todo.list.queryOptions({ input: { slug } }));Testing Routes
Routers have node:test tests next to the code (*-router.test.ts):
pnpm -F @repo/service testFrontend Development
Web App (Next.js)
apps/web/src/app/
├── (auth)/ # Login, register, password reset
├── (dashboard)/ # Protected org dashboard
├── (marketing)/ # Public landing page
├── (docs)/ # These docs
└── api/ # Route handlers (auth, orpc, account, search, health)Mobile App (Expo)
apps/mobile/src/
├── app/ # Expo Router screens
└── utils/ # auth, oRPC client, base-urlAdding a Feature
- Schema - table in
packages/db - API - router in
packages/service - UI - components in
packages/uiif shared - Screens - per app
- Test - router tests + manual pass on each platform
UI Components
Using Shared Components
Import explicit paths:
import { Button } from "@repo/ui/components/button";
import { cn } from "cn";
<Button variant="outline" onClick={handleClick}>
Save Changes
</Button>Adding Components
Pull from the shadcn registry (base-vega style):
cd packages/ui && pnpm dlx shadcn@latest add <component>Or hand-write in packages/ui/src/components/ - kebab-case filenames.
Code Quality
Init uses the oxc toolchain - fast, zero-config-ish:
- oxlint - Linting
- oxfmt - Formatting
- TypeScript - Type checking
- Turbo - Cached builds
# Check everything
pnpm lint && pnpm typecheck
# Fix what's fixable
pnpm lint:fix && pnpm format:fixDebugging
Web
- Browser DevTools + React DevTools
- oRPC logs failed requests in dev (onError interceptor)
- TanStack Query Devtools ships in the tree
Mobile
- Expo DevTools and device logs
- Console output in the Metro terminal
API / Database
- Server logs in the
pnpm devterminal pnpm -F db studiofor Drizzle Studio - browse and edit data
Common Issues
TypeScript errors
pnpm build && pnpm typecheckDatabase connection issues
pnpm db:stop && pnpm db:start
cat .env # POSTGRES_URL set?Build failures
pnpm clean && pnpm install && pnpm buildEnvironment Variables
One root .env file. Apps and packages load it via dotenv -e ../../.env. Turborepo declares the allow-list in turbo.json (globalEnv).
BETTER_AUTH_SECRET=...
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
BLOB_READ_WRITE_TOKEN=...POSTGRES_URL is the only database input - local Supabase in dev, a Supabase project (or any Postgres) in production. Locally it isn't in .env: pnpm db:start has the Supabase CLI write it into .env.local (POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:24571/postgres, say), and the scripts that need the database load .env.local before .env. Every checkout and git branch gets its own database on its own port, so clones, worktrees and parallel agents run their databases side by side. To use a hosted database instead, set POSTGRES_URL in .env and delete .env.local.
BLOB_READ_WRITE_TOKEN is for avatar uploads via Vercel Blob, which has no local emulator. Leave it unset offline - the avatar route returns 501 with a pointer, everything else works.
Production values live in your deployment platform, plus .env.production.local for pnpm db:push-remote.
Next Steps
- Explore the architecture - Folder structure
- Understand the API - oRPC guide
- Learn the auth flow - better-auth setup