QUESTPIE
Examples

TanStack Barbershop

End-to-end tour of the barbershop booking app - collections, routes, jobs, admin, and frontend.

The barbershop example is a complete booking app built with TanStack Start and QUESTPIE. It demonstrates every major feature of the framework.

Source: examples/tanstack-barbershop/

Project Structure

examples/tanstack-barbershop/
├── questpie.config.ts                        # CLI config (re-exports)
├── src/
│   ├── questpie/
│   │   ├── server/
│   │   │   ├── questpie.config.ts            # Runtime config
│   │   │   ├── modules.ts                    # adminModule + auditModule
│   │   │   ├── auth.ts                       # Better Auth config
│   │   │   ├── locale.ts                     # en + sk locales
│   │   │   ├── admin-locale.ts               # Admin UI locales
│   │   │   ├── sidebar.ts                    # 6 sidebar sections
│   │   │   ├── dashboard.ts                  # Stats, charts, timeline
│   │   │   ├── branding.ts                   # "Barbershop Control"
│   │   │   ├── collections/
│   │   │   │   ├── barbers.ts                # Complex: working hours, social links, M2M
│   │   │   │   ├── services.ts               # Localized, relations
│   │   │   │   ├── appointments.ts           # Hooks, virtual fields, visibility
│   │   │   │   ├── barber-services.ts        # Junction table
│   │   │   │   ├── reviews.ts                # Conditional fields
│   │   │   │   ├── blog-posts.ts             # Service injection in hooks
│   │   │   │   └── pages.ts                  # Blocks, preview
│   │   │   ├── globals/
│   │   │   │   └── site-settings.ts          # Nested config, versioning
│   │   │   ├── routes/
│   │   │   │   ├── create-booking.ts          # Business logic
│   │   │   │   ├── get-available-time-slots.ts
│   │   │   │   ├── get-active-barbers.ts
│   │   │   │   └── get-revenue-stats.ts
│   │   │   ├── jobs/
│   │   │   │   ├── send-appointment-confirmation.ts
│   │   │   │   ├── send-appointment-cancellation.ts
│   │   │   │   └── notify-blog-subscribers.ts
│   │   │   ├── services/
│   │   │   │   └── blog.ts                    # Singleton service
│   │   │   ├── blocks/                        # Content blocks
│   │   │   ├── emails/                        # Email templates
│   │   │   └── .generated/                    # Codegen output
│   │   └── admin/
│   │       ├── admin.ts                       # Re-exports generated admin config
│   │       ├── hooks.ts                       # Typed React hooks
│   │       ├── .generated/                    # Codegen output
│   │       │   └── client.ts                  # Admin client config
│   │       └── blocks/                        # Block renderers
│   ├── lib/
│   │   ├── client.ts                          # Client SDK setup
│   │   └── server-helpers.ts                  # Server context helpers
│   └── routes/
│       ├── api/$.ts                           # API catch-all
│       └── _app/
│           ├── index.tsx                      # Homepage (page blocks)
│           └── booking.tsx                    # Multi-step booking wizard

Key Patterns

Runtime Config

questpie.config.ts
import { adminPlugin } from "@questpie/admin/plugin";
import { pgBossAdapter, runtimeConfig, SmtpAdapter } from "questpie";

export default runtimeConfig({
	plugins: [adminPlugin()],
	app: { url: process.env.APP_URL || "http://localhost:3000" },
	db: { url: process.env.DATABASE_URL || "postgres://localhost/barbershop" },
	storage: { basePath: "/api" },
	secret: process.env.BETTER_AUTH_SECRET || "demo-secret",
	queue: { adapter: pgBossAdapter({ connectionString: DATABASE_URL }) },
});

Collection with Hooks

collections/appointments.ts
export const appointments = collection("appointments")
	.fields(({ f }) => ({
		customer: f.relation("user").required(),
		barber: f.relation("barbers").required(),
		service: f.relation("services").required(),
		scheduledAt: f.datetime().required(),
		status: f
			.select([
				{ value: "pending", label: { en: "Pending" } },
				{ value: "confirmed", label: { en: "Confirmed" } },
				{ value: "completed", label: { en: "Completed" } },
				{ value: "cancelled", label: { en: "Cancelled" } },
			])
			.required()
			.default("pending"),
		cancellationReason: f.textarea(),
		displayTitle: f.text()
			.virtual(sql`(SELECT name FROM "user" WHERE id = appointments.customer)
      || ' - ' || TO_CHAR(appointments."scheduledAt", 'YYYY-MM-DD HH24:MI')`),
	}))
	.form(({ v, f }) =>
		v.collectionForm({
			fields: [
				{
					type: "section",
					fields: [
						f.cancelledAt,
						{
							field: f.cancellationReason,
							hidden: ({ data }) => data.status !== "cancelled",
						},
					],
				},
			],
		}),
	)
	.hooks({
		afterChange: async ({ data, operation, queue }) => {
			if (operation === "create") {
				await queue.sendAppointmentConfirmation.publish({
					appointmentId: data.id,
					customerId: data.customer,
				});
			}
		},
	});

Service Injection

services/blog.ts
import { service } from "questpie";

export default service({
	lifecycle: "singleton",
	create: () => ({
		computeReadingTime(content: string): number {
			/* ... */
		},
		generateSlug(title: string): string {
			/* ... */
		},
		extractExcerpt(content: string): string {
			/* ... */
		},
	}),
});

Used in hooks via services — no imports:

.hooks({
  beforeChange: async ({ data, services }) => {
    data.slug = services.blog.generateSlug(data.title);
    data.readingTime = services.blog.computeReadingTime(data.content);
  },
})

Client Setup

lib/client.ts
import { createClient } from "questpie/client";
import type { AppConfig } from "#questpie";

export const client = createClient<AppConfig>({
	baseURL:
		typeof window !== "undefined"
			? window.location.origin
			: process.env.APP_URL,
	basePath: "/api",
});

Frontend Booking

routes/_app/booking.tsx
import { client } from "@/lib/client";
import { useMutation, useQuery } from "@tanstack/react-query";

function BookingWizard() {
	const { data: barbers } = useQuery({
		queryKey: ["barbers"],
		queryFn: () => client.routes.getActiveBarbers({}),
	});

	const booking = useMutation({
		mutationFn: (data) => client.routes.createBooking(data),
		onSuccess: (result) => {
			// Navigate to confirmation
		},
	});

	// Multi-step wizard UI...
}

What This Example Demonstrates

FeatureWhere
Complex fields (objects, arrays, select)collections/barbers.ts
Many-to-many relationsbarbers ↔ barberServices ↔ services
Virtual SQL fieldsappointments.tsdisplayTitle
Lifecycle hooksappointments.tsafterChange
Service injectionblog.ts → used in blog-posts.ts hooks
Background jobsjobs/send-appointment-confirmation.ts
Email templatesemails/appointment-confirmation.ts
Server routesroutes/create-booking.ts
Admin dashboarddashboard.ts — stats, charts, timeline
Admin sidebarsidebar.ts — 6 sections, mixed items
Content blocksblocks/hero.ts, pages.ts with f.blocks()
Conditional visibilityappointments.ts form → hidden
Computed form fieldsbarbers.ts form → slug from name
Multi-language (i18n)Labels, content, admin UI
Access controlsite-settings.ts → admin-only update
Live previewbarbers.ts, pages.ts → preview URL

On this page