Overview

Local Development

Daily workflows, debugging, and conventions for building with Init.

Development Workflow

Starting Your Session

  1. Start the database:

    pnpm db:start
  2. Start dev servers:

    pnpm dev
  3. Open your tools:

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 only

# Database
pnpm db:reset        # Reset local database
pnpm db:push         # Push schema changes

# Code quality
pnpm lint            # oxlint
pnpm typecheck       # TypeScript
pnpm format          # oxfmt
pnpm test            # node:test

Monorepo Structure

apps/
├── web/          # Next.js web app
├── mobile/       # Expo mobile app
├── extension/    # Chrome extension (WXT)
└── desktop/      # Electron desktop app

packages/
├── api/          # oRPC routers + better-auth
├── db/           # Drizzle schema + local Postgres compose
└── ui/           # Shared React components

Every platform shares the API, auth, and database packages. Write business logic once.

Working with the Database

Schema Changes

  1. Modify schema in packages/db/src/drizzle-schema.ts
  2. Push changes:
    pnpm db:push
  3. 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

Add a feature folder in packages/api/src/ with a router and Zod schema:

// packages/api/src/post/post-router.ts
export const postRouter = {
  list: organizationProcedure(organizationInput).handler(async ({ context }) => {
    return context.db.query.post.findMany({
      where: { organizationId: context.organization.id },
    });
  }),
};

organizationProcedure (from ../orpc) takes the procedure's input schema and proves membership before the handler runs. Register in packages/api/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/api test

Frontend 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-url

Adding a Feature

  1. Schema - table in packages/db
  2. API - router in packages/api
  3. UI - components in packages/ui if shared
  4. Screens - per app
  5. 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:fix

Debugging

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 dev terminal
  • pnpm -F db studio for Drizzle Studio - browse and edit data

Common Issues

TypeScript errors

pnpm build && pnpm typecheck

Database connection issues

pnpm db:stop && pnpm db:start
cat .env   # POSTGRES_URL set?

Build failures

pnpm clean && pnpm install && pnpm build

Environment Variables

One root .env file. Apps and packages load it via dotenv -e ../../.env. Turborepo declares the allow-list in turbo.json (globalEnv).

POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
POSTGRES_PORT=54322
COMPOSE_PROJECT_NAME=init
BETTER_AUTH_SECRET=...
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
BLOB_READ_WRITE_TOKEN=...

POSTGRES_URL is the only database input - the local Docker container in dev, Vercel Postgres (or any Postgres) in production. COMPOSE_PROJECT_NAME namespaces that container's volume and POSTGRES_PORT is the host port it publishes; together they let two clones run their databases at the same time. pnpm bootstrap writes both — the project name from the repo folder, the port as the first free one from 54322 up.

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

  1. Explore the architecture - Folder structure
  2. Understand the API - oRPC guide
  3. Learn the auth flow - better-auth setup

On this page