Overview

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

  1. Start the database (local Supabase; points .env.local at this branch's database):

    pnpm db:start

    Each git branch has its own database. On a branch you haven't used before it starts empty: pnpm db:push && pnpm db:seed.

  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 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 test

Monorepo 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 components

Every 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

  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

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 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/service
  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).

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

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

On this page