Skip to main content

Getting Started

Prerequisites

Ensure you have the following installed:
  • Node.js: >= 20
  • Bun: Latest version (recommended) or npm/pnpm
  • Git: For version control
  • PostgreSQL: Via Neon (cloud) or local instance

Initial Setup

  1. Clone the repository:
git clone https://github.com/featul/featul.git
cd featul
  1. Install dependencies:
bun install
  1. Set up environment variables:
Copy the example env file and configure:
cp apps/app/.env.example apps/app/.env.local
Required variables:
# Database
DATABASE_URL=postgresql://...

# Authentication
BETTER_AUTH_SECRET=your-secret-key
BETTER_AUTH_URL=http://localhost:3000
AUTH_COOKIE_DOMAIN=.localhost

# App URLs
NEXT_PUBLIC_APP_URL=http://app.localhost:3000
NEXT_PUBLIC_API_URL=http://app.localhost:3000/api

# OAuth (optional)
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...

# Storage (optional)
R2_ACCOUNT_ID=...
R2_ACCESS_KEY_ID=...
R2_SECRET_ACCESS_KEY=...
R2_BUCKET=...
R2_PUBLIC_BASE_URL=...

# Email (optional)
RESEND_API_KEY=...
  1. Set up the database:
bun run db:push  # Push schema to database
  1. Start development servers:
bun dev  # Starts all apps
Or start specific apps:
bun run app:dev  # Main app only
bun run web:dev  # Marketing site only

Hosts Configuration

For local subdomain testing, add to /etc/hosts:
127.0.0.1 app.localhost
127.0.0.1 www.localhost
127.0.0.1 test.localhost
Access the app at:
  • Main app: http://app.localhost:3000
  • Workspace: http://test.localhost:3000 (replace test with your workspace slug)

Development Workflow

Creating a Feature

  1. Create a feature branch:
git checkout -b feature/your-feature-name
  1. Make your changes:
Follow the project structure:
  • API changes: packages/api/src/router/
  • Database changes: packages/db/schema/
  • UI components: packages/ui/src/
  • App features: apps/app/src/
  1. Test your changes:
bun run check-types  # Type checking
bun run lint         # Linting
  1. Commit your changes:
git add .
git commit -m "feat: add new feature"
Follow conventional commit format:
  • feat: - New feature
  • fix: - Bug fix
  • docs: - Documentation changes
  • style: - Code style changes (formatting)
  • refactor: - Code refactoring
  • test: - Adding tests
  • chore: - Build process or tooling changes
  1. Push and create a PR:
git push origin feature/your-feature-name
Then create a Pull Request on GitHub.

Database Changes

  1. Modify schema files in packages/db/schema/
  2. Generate migration:
bun run db:generate
  1. Review generated SQL in packages/db/drizzle/
  2. Test migration:
bun run db:push  # For development
# OR
bun run db:migrate  # For production-like testing
  1. Commit both schema and migration files

Adding a New API Endpoint

  1. Create validator in packages/api/src/validators/:
// packages/api/src/validators/my-feature.ts
import { z } from "zod"

export const createMyFeatureInputSchema = z.object({
  name: z.string().min(1).max(100),
  description: z.string().optional(),
})
  1. Add router in packages/api/src/router/:
// packages/api/src/router/my-feature.ts
import { j, privateProcedure } from "../jstack"
import { createMyFeatureInputSchema } from "../validators/my-feature"

export function createMyFeatureRouter() {
  return j.router({
    create: privateProcedure
      .input(createMyFeatureInputSchema)
      .post(async ({ ctx, input, c }) => {
        // Implementation
        return c.superjson({ data: result })
      }),
  })
}
  1. Register router in packages/api/src/index.ts:
const routerImports = {
  // ... existing routers
  myFeature: () => import("./router/my-feature").then((m) => m.createMyFeatureRouter()),
}

const appRouter = j.mergeRouters(api, {
  // ... existing routers
  myFeature: routerImports.myFeature,
})
  1. Use in frontend:
import { client } from "@featul/api/client"

const result = await client.myFeature.create.$post({
  name: "Test",
  description: "Description",
})

Adding a UI Component

  1. Create component in packages/ui/src/:
// packages/ui/src/my-component.tsx
import * as React from "react"
import { cn } from "./utils"

export interface MyComponentProps {
  children: React.ReactNode
  variant?: "default" | "primary"
}

export function MyComponent({ children, variant = "default" }: MyComponentProps) {
  return (
    <div className={cn("my-component", variant)}>
      {children}
    </div>
  )
}
  1. Export from package:
// packages/ui/src/index.ts
export { MyComponent } from "./my-component"
  1. Use in app:
import { MyComponent } from "@featul/ui"

<MyComponent variant="primary">Content</MyComponent>

Code Standards

TypeScript

  • Use strict mode: All packages use strict: true
  • No any: Avoid using any, use unknown if necessary
  • Explicit types: Define interfaces and types for complex objects
  • Null safety: Use optional chaining and nullish coalescing
// Good
interface User {
  id: string
  name: string
  email: string | null
}

const userName = user?.name ?? "Anonymous"

// Bad
const user: any = { ... }
const userName = user.name || "Anonymous"

React

  • Use hooks: Prefer functional components with hooks
  • Server Components: Use React Server Components by default in Next.js
  • Client Components: Mark with "use client" only when needed
  • Composition: Prefer composition over inheritance
// Server Component (default)
export default async function Page() {
  const data = await fetchData()
  return <div>{data}</div>
}

// Client Component (when interactivity needed)
"use client"

import { useState } from "react"

export function InteractiveComponent() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

Database

  • Use transactions: For multi-step operations
  • Add indexes: For frequently queried columns
  • Validate constraints: Use database constraints, not just app logic
  • Cascading deletes: Define foreign key cascade behavior
// Use transactions for related operations
await db.transaction(async (tx) => {
  const [workspace] = await tx.insert(workspace).values(data).returning()
  await tx.insert(workspaceMember).values({ workspaceId: workspace.id, userId })
})

API Design

  • RESTful naming: Use resource-based names
  • Input validation: Validate all inputs with Zod
  • Error handling: Return appropriate HTTP status codes
  • Response consistency: Use consistent response formats
// Good endpoint structure
export function createPostRouter() {
  return j.router({
    list: publicProcedure.get(...),      // GET /api/post/list
    byId: publicProcedure.get(...),      // GET /api/post/byId
    create: privateProcedure.post(...),  // POST /api/post/create
    update: privateProcedure.post(...),  // POST /api/post/update
    delete: privateProcedure.post(...),  // POST /api/post/delete
  })
}

Styling

  • Tailwind CSS: Use utility classes
  • Component variants: Use class-variance-authority for variants
  • Responsive: Mobile-first responsive design
  • Dark mode: Support dark mode with next-themes
import { cva } from "class-variance-authority"

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md font-medium",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        outline: "border border-input hover:bg-accent",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 px-3",
        lg: "h-11 px-8",
      },
    },
  }
)

Testing Guidelines

While not currently implemented, here’s the recommended approach:

Unit Tests

import { describe, test, expect } from "vitest"
import { normalizeSlug } from "./utils"

describe("normalizeSlug", () => {
  test("converts to lowercase", () => {
    expect(normalizeSlug("Hello World")).toBe("hello-world")
  })
  
  test("removes special characters", () => {
    expect(normalizeSlug("Hello@World!")).toBe("hello-world")
  })
})

Integration Tests

import { testClient } from "hono/testing"
import appRouter from "@featul/api"

test("create workspace", async () => {
  const client = testClient(appRouter)
  const res = await client.workspace.create.$post({
    name: "Test Workspace",
    slug: "test",
    domain: "https://test.com",
    timezone: "UTC",
  })
  
  expect(res.status).toBe(200)
  const data = await res.json()
  expect(data.workspace.slug).toBe("test")
})

Pull Request Guidelines

Before Submitting

  • Code follows style guidelines
  • Type checking passes (bun run check-types)
  • Linting passes (bun run lint)
  • Code is formatted (bun run format)
  • Tested locally
  • Database migrations included (if schema changed)
  • Documentation updated (if needed)

PR Description Template

## Description

Brief description of changes.

## Type of Change

- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update

## Testing

How was this tested?

## Screenshots (if applicable)

## Related Issues

Fixes #123

Code Review Process

  1. Automated checks must pass (types, lint, build)
  2. At least one approval required
  3. Address all feedback
  4. Squash and merge to main

Community

Getting Help

  • GitHub Issues: Bug reports and feature requests
  • GitHub Discussions: Questions and community support
  • Discord: Real-time chat (if available)

Reporting Bugs

When reporting bugs, include:
  1. Description: Clear description of the issue
  2. Steps to reproduce: Minimal steps to reproduce
  3. Expected behavior: What you expected to happen
  4. Actual behavior: What actually happened
  5. Environment: OS, Node version, browser
  6. Screenshots: If applicable

Suggesting Features

When suggesting features:
  1. Use case: Describe the problem you’re solving
  2. Proposed solution: How you think it should work
  3. Alternatives: Other solutions you considered
  4. Additional context: Mockups, examples, etc.

License

By contributing, you agree that your contributions will be licensed under the same license as the project.

Build docs developers (and LLMs) love