Quick Start Guide - Eventuall Development

Last Updated: 2025-09-08

This guide helps you get started quickly with common development tasks in the Eventuall project.

Prerequisites

  • Node.js >= 22
  • pnpm 10.12.1
  • Terraform >= 1.5.0
  • Cloudflare account with API tokens
  • Git configured with SSH access

Initial Setup

1. Clone and Install

# Clone the repository
git clone git@github.com:eventuall/eventuall-app.git
cd eventuall-app

# Install dependencies
pnpm install

# Set up infrastructure (creates D1 databases, R2 buckets, etc.)
./scripts/setup.sh

2. Environment Variables

Create .env.local in apps/webapp/:

# Authentication
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-here
AUTH_GOOGLE_ID=your-google-oauth-id
AUTH_GOOGLE_SECRET=your-google-oauth-secret

# Database (auto-generated by setup.sh)
DATABASE_URL=./.wrangler/state/v3/d1/miniflare-D1DatabaseObject/

# LiveKit
LIVEKIT_API_KEY=your-livekit-key
LIVEKIT_API_SECRET=your-livekit-secret
LIVEKIT_URL=wss://your-livekit-server

# Twilio
TWILIO_ACCOUNT_SID=your-twilio-sid
TWILIO_AUTH_TOKEN=your-twilio-token

# Cloudflare (auto-configured)
CLOUDFLARE_ACCOUNT_ID=your-account-id
CLOUDFLARE_API_TOKEN=your-api-token

3. Start Development

# Start the development server
pnpm dev

# Open http://localhost:3000

Common Development Tasks

Creating a New Page

Server Component Page (Default)

// apps/webapp/src/app/events/[eventId]/details/page.tsx
import { api } from "@/trpc/server";

export default async function EventDetailsPage(props: {
  params: Promise<{ eventId: string }>;
}) {
  const params = await props.params;
  const event = await api.event.getEventById({ eventId: params.eventId });
  
  return (
    <div className="container mx-auto p-6">
      <h1 className="text-2xl font-bold">{event.name}</h1>
      <p>{event.description}</p>
    </div>
  );
}

Client Component Page

// apps/webapp/src/app/events/[eventId]/controls/page.tsx
"use client";

import { useState } from "react";
import { api } from "@/trpc/client";
import { Button } from "@/components/ui/button";

export default function EventControlsPage() {
  const [isPlaying, setIsPlaying] = useState(false);
  
  return (
    <div className="flex gap-4">
      <Button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? "Pause" : "Play"}
      </Button>
    </div>
  );
}

Adding a New tRPC Endpoint

1. Create the Router

// apps/webapp/src/server/api/routers/ticket.router.ts
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";

export const ticketRouter = createTRPCRouter({
  // Query example
  getTicketById: protectedProcedure
    .input(z.object({ ticketId: z.string() }))
    .query(async ({ input, ctx }) => {
      return ctx.db.query.tickets.findFirst({
        where: eq(tickets.id, input.ticketId),
      });
    }),
  
  // Mutation example
  createTicket: protectedProcedure
    .input(z.object({
      eventId: z.string(),
      name: z.string().min(1),
      price: z.number().positive(),
    }))
    .mutation(async ({ input, ctx }) => {
      const [ticket] = await ctx.db
        .insert(tickets)
        .values(input)
        .returning();
      
      return ticket;
    }),
});

2. Add to Root Router

// apps/webapp/src/server/api/root.ts
import { ticketRouter } from "./routers/ticket.router";

export const appRouter = createTRPCRouter({
  event: eventRouter,
  user: userRouter,
  ticket: ticketRouter, // Add here
  // ... other routers
});

3. Use in Components

// In a component
const { data: ticket } = api.ticket.getTicketById.useQuery({ 
  ticketId: "123" 
});

const createTicket = api.ticket.createTicket.useMutation();

Creating a Server Action

// apps/webapp/src/server/actions/ticket.ts
"use server";

import { authenticatedActionClient } from "@/server/safe-action";
import { z } from "zod";

const purchaseTicketSchema = z.object({
  ticketId: z.string(),
  quantity: z.number().positive(),
});

export const purchaseTicketAction = authenticatedActionClient
  .metadata({ actionName: "purchaseTicket" })
  .schema(purchaseTicketSchema)
  .action(async ({ parsedInput, ctx }) => {
    // Perform the purchase logic
    const result = await processPayment(parsedInput);
    
    if (!result.success) {
      return { error: "Payment failed" };
    }
    
    // Update database
    await ctx.db.insert(purchases).values({
      userId: ctx.user.id,
      ticketId: parsedInput.ticketId,
      quantity: parsedInput.quantity,
    });
    
    return { success: true, purchaseId: result.id };
  });

Adding a New UI Component

1. Create the Component

// apps/webapp/src/components/ui/event-card.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";

interface EventCardProps {
  title: string;
  description: string;
  status: "draft" | "active" | "ended";
  className?: string;
}

export function EventCard({ 
  title, 
  description, 
  status, 
  className 
}: EventCardProps) {
  return (
    <Card className={cn("hover:shadow-lg transition-shadow", className)}>
      <CardHeader>
        <div className="flex items-center justify-between">
          <CardTitle>{title}</CardTitle>
          <Badge variant={status === "active" ? "default" : "secondary"}>
            {status}
          </Badge>
        </div>
      </CardHeader>
      <CardContent>
        <p className="text-muted-foreground">{description}</p>
      </CardContent>
    </Card>
  );
}

2. Use the Component

import { EventCard } from "@/components/ui/event-card";

export default function EventsPage() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
      <EventCard
        title="Summer Concert"
        description="Live music event"
        status="active"
      />
    </div>
  );
}

Form with Validation

// apps/webapp/src/components/forms/event-form.tsx
"use client";

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";

const eventSchema = z.object({
  name: z.string().min(3, "Name must be at least 3 characters"),
  date: z.string().refine((date) => new Date(date) > new Date(), {
    message: "Date must be in the future",
  }),
});

type EventFormData = z.infer<typeof eventSchema>;

export function EventForm() {
  const form = useForm<EventFormData>({
    resolver: zodResolver(eventSchema),
    defaultValues: {
      name: "",
      date: "",
    },
  });

  const onSubmit = async (data: EventFormData) => {
    console.log("Form data:", data);
    // Submit to server action or tRPC mutation
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Event Name</FormLabel>
              <FormControl>
                <Input placeholder="Summer Concert" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <FormField
          control={form.control}
          name="date"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Event Date</FormLabel>
              <FormControl>
                <Input type="datetime-local" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <Button type="submit" disabled={form.formState.isSubmitting}>
          {form.formState.isSubmitting ? "Creating..." : "Create Event"}
        </Button>
      </form>
    </Form>
  );
}

Database Operations

Schema Definition

// apps/webapp/src/server/db/schema.ts
export const tickets = sqliteTable("tickets", {
  id: text("id")
    .primaryKey()
    .$defaultFn(() => createId()),
  eventId: text("eventId")
    .notNull()
    .references(() => events.id),
  name: text("name").notNull(),
  price: integer("price").notNull(),
  quantity: integer("quantity").notNull(),
  createdAt: integer("created_at", { mode: "timestamp" })
    .default(sql`CURRENT_TIMESTAMP`)
    .notNull(),
});

Query Examples

// Simple query
const tickets = await db.query.tickets.findMany({
  where: eq(tickets.eventId, eventId),
});

// Query with relations
const eventWithTickets = await db.query.events.findFirst({
  where: eq(events.id, eventId),
  with: {
    tickets: true,
    rooms: true,
  },
});

// Insert
const [newTicket] = await db
  .insert(tickets)
  .values({
    eventId: "event123",
    name: "VIP Pass",
    price: 100,
    quantity: 50,
  })
  .returning();

// Update
await db
  .update(tickets)
  .set({ quantity: 25 })
  .where(eq(tickets.id, ticketId));

// Delete
await db.delete(tickets).where(eq(tickets.id, ticketId));

// Transaction
await db.transaction(async (tx) => {
  await tx.insert(events).values(eventData);
  await tx.insert(tickets).values(ticketData);
});

Testing

Unit Tests

// apps/webapp/src/server/services/__tests__/event.service.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { EventService } from "../event.service";

describe("EventService", () => {
  beforeEach(() => {
    // Setup test database
  });

  it("should create an event", async () => {
    const event = await EventService.create({
      name: "Test Event",
      accountId: "account123",
    });
    
    expect(event).toBeDefined();
    expect(event.name).toBe("Test Event");
  });
});

Component Tests

// apps/webapp/src/components/__tests__/event-card.test.tsx
import { render, screen } from "@testing-library/react";
import { EventCard } from "../ui/event-card";

describe("EventCard", () => {
  it("renders event information", () => {
    render(
      <EventCard
        title="Test Event"
        description="Test description"
        status="active"
      />
    );
    
    expect(screen.getByText("Test Event")).toBeInTheDocument();
    expect(screen.getByText("Test description")).toBeInTheDocument();
    expect(screen.getByText("active")).toBeInTheDocument();
  });
});

Debugging Tips

1. Server Component Debugging

// Add console.logs - they appear in terminal
export default async function Page() {
  const data = await api.event.list();
  console.log("Events data:", data); // Shows in terminal
  return <div>...</div>;
}

2. Client Component Debugging

// Use browser DevTools
"use client";
export default function Component() {
  console.log("Component rendered"); // Shows in browser console
  
  // Use React DevTools for state inspection
  const [state, setState] = useState();
  
  // Add debug UI in development
  if (process.env.NODE_ENV === "development") {
    return <pre>{JSON.stringify(state, null, 2)}</pre>;
  }
}

3. tRPC Debugging

// Enable tRPC logging
export const appRouter = createTRPCRouter({
  // ...
}).middleware(async ({ path, type, next }) => {
  console.log(`tRPC ${type} ${path}`);
  const result = await next();
  console.log("Result:", result);
  return result;
});

4. Database Query Debugging

// Log SQL queries
const result = await db
  .select()
  .from(events)
  .where(eq(events.id, eventId));

console.log("SQL:", result.toSQL()); // Shows generated SQL

Troubleshooting

Common Issues and Solutions

Issue: "Session not found" error

# Solution: Ensure NEXTAUTH_SECRET is set
echo "NEXTAUTH_SECRET=$(openssl rand -base64 32)" >> .env.local

Issue: Database migrations not applying

# Solution: Regenerate and apply migrations
pnpm generate
pnpm migrate:local

Issue: TypeScript errors after adding new tRPC router

# Solution: Restart TypeScript server
# In VS Code: Cmd+Shift+P > "TypeScript: Restart TS Server"

Issue: Cloudflare tunnel not connecting

# Solution: Restart tunnel
cloudflared tunnel run eventuall-local

Useful Commands Reference

# Development
pnpm dev                 # Start dev server
pnpm build              # Build for production
pnpm start              # Start production server
pnpm preview            # Preview production build

# Database
pnpm generate           # Generate migrations
pnpm migrate:local      # Apply migrations locally
pnpm migrate:remote     # Apply to remote database

# Testing & Quality
pnpm test              # Run tests once
pnpm test:watch        # Run tests in watch mode
pnpm lint              # Run ESLint
pnpm check-types       # TypeScript type checking
pnpm format            # Format with Prettier

# Infrastructure
./scripts/setup.sh      # Setup all infrastructure
terraform plan          # Preview infrastructure changes
terraform apply         # Apply infrastructure changes
terraform destroy       # Destroy infrastructure

# Utilities
pnpm cf-typegen        # Generate Cloudflare types
npx drizzle-kit studio # Open Drizzle Studio (DB viewer)

Next Steps

  1. Read the Developer Onboarding Guide for in-depth explanations
  2. Review the Architecture Overview for system design
  3. Join the team Slack channel for questions
  4. Check the project board for available tasks

Need Help?

  • Check existing documentation in /docs
  • Search the codebase for similar implementations
  • Ask in the team Slack channel
  • Review closed PRs for examples