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
- Read the Developer Onboarding Guide for in-depth explanations
- Review the Architecture Overview for system design
- Join the team Slack channel for questions
- 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