An events app pulls together more AppEngine modules than any other vertical. You're booking sessions, selling tickets, gating content by ticket type, hosting a discussion forum, running 1:1 networking, sending push notifications, and probably running an in-person scanner at the door. This tutorial walks through the event_app pattern (Flutter + AppEngine) and translates the wiring to a web stack.
What you're building
A conference/community app with:
- Public event browsing — landing pages, agenda, speakers
- Ticketing + checkout — multiple ticket types, discount codes, attendee registration
- Authenticated attendee shell — session schedule, my tickets, networking, chat
- Community feed — posts, announcements, replies
- In-app messaging — DMs and chat-rooms via the chat gateway
- Admin tools — check-in, attendee list, content moderation
Five AppEngine modules carry it: events, storefront (for tickets), community (posts and connections), chat, and CRM (attendee records).
Module map
| What you see | AppEngine endpoint cluster |
|---|---|
| Event landing | client/events/{id} |
| Ticket types + checkout | client/events/{id}/ticket-types, storefront/checkout-cart |
| Schedule + sessions | client/events/{id}/sessions, client/events/{id}/schedule |
| Speakers, sponsors | client/events/{id}/participants?type=speaker |
| Community feed | data/post, data/post-comment |
| Chat | Socket.IO via ChatGateway |
| My tickets | data/order filtered by customerId |
| Check-in | client/events/{id}/check-in |
Built-in collections (post, order, ticket) plus the client/events/* shortcut endpoints — no custom collections needed for the basic app.
Prerequisites
- AppEngine instance, org id
- Next.js 16 + Tailwind project (web) or a Flutter project (mobile)
- Stripe set up for ticket payments
- Push notification keys (FCM for Android, APNs for iOS) if mobile
Step-by-step
- 1
Create the event
In the admin UI, Events → New event. The minimum:
// POST /repository/create/event { "data": { "name": "DevCon 2026", "startDate": "2026-09-15T09:00:00Z", "endDate": "2026-09-17T18:00:00Z", "venue": { "name": "Moscone West", "city": "San Francisco" }, "description": "...", "coverImage": "https://...", "ticketTypes": [ { "id": "early", "name": "Early bird", "price": 299, "available": 200 }, { "id": "regular", "name": "Regular", "price": 499, "available": 500 }, { "id": "vip", "name": "VIP + workshops", "price": 999, "available": 50 } ], "state": "draft" } }Flip
statetopublishedwhen you're ready. Until then,client/events/browsewon't return it.Sessions are nested children —
data/event-sessionrecords witheventIdpointing at the parent. Add tracks and rooms via the admin UI; the schedule endpoint stitches it together. - 2
Public event landing page
Web side. A server component pulls the event detail and renders.
// src/app/events/[id]/page.tsx import { eventsAPI } from '@/lib/events-api'; export default async function EventPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; const detail = await eventsAPI.getEventDetail(id); if (!detail) return <div>Not found</div>; const { event, ticketTypes } = detail; return ( <div className="mx-auto max-w-4xl py-12"> <img src={event.coverImage} alt="" className="aspect-video rounded-lg object-cover" /> <h1 className="mt-6 text-3xl font-bold">{event.name}</h1> <p className="text-gray-500"> {new Date(event.startDate).toLocaleDateString()} — {event.venue.name} </p> <p className="mt-4">{event.description}</p> <h2 className="mt-12 text-xl font-semibold">Tickets</h2> <div className="mt-4 grid gap-4"> {ticketTypes.map((t) => ( <TicketCard key={t.id} ticketType={t} eventId={id} /> ))} </div> </div> ); }eventsAPIis the wrapper frombase-app/src/lib/events-api.ts— copy it; it's already typed againstclient/events/*. - 3
Ticket purchase flow
Tickets are products with extra metadata. The cart endpoint accepts ticket types like any other line item:
// src/lib/ticket-checkout.ts import { storefrontAPI } from '@/lib/storefront-api'; export async function buyTicket(eventId: string, ticketTypeId: string, quantity: number) { const cart = await storefrontAPI.addToCart({ productId: `ticket:${eventId}:${ticketTypeId}`, quantity, metadata: { eventId, ticketTypeId }, }); return storefrontAPI.startCheckout(cart.id); }The product id
ticket:{eventId}:{ticketTypeId}is a synthetic id AppEngine resolves via the events module — no need to create aproductrecord per ticket type. Stock is decremented onticketTypes[].available.The rest of the checkout is the storefront tutorial — payment intent, success page, order record. The order's
metadata.eventIdandmetadata.ticketTypeIdis what later attaches the attendee to the event. - 4
Issue tickets and confirmation
After
payment.succeeded, an AppEngine workflow:- Creates a
ticketrecord per quantity, with a uniquecode(UUID or alphanumeric). - Generates a QR code (encoded
code) on AppEngine and stores the URL. - Sends an email with the ticket attachment.
- Sends a push notification "Welcome to DevCon!".
- Adds the attendee to the event's
attendeesassociation.
You don't write any of that in your Next.js app — it's a workflow defined once. See Automation overview.
- Creates a
- 5
Attendee shell — schedule
Once authenticated and registered, the attendee sees their schedule. Two queries: the event's full session list, and the attendee's
bookmarks(sessions they've added).// src/app/(attendee)/schedule/page.tsx import { eventsAPI } from '@/lib/events-api'; import { repositoryAPI } from '@/lib/repository-api'; export default async function SchedulePage() { const event = await getCurrentEvent(); const [sessions, bookmarks] = await Promise.all([ eventsAPI.getEventSessions(event.id), repositoryAPI.list('bookmark', { filter: { eventId: event.id, customerId: 'self' } }), ]); const bookmarkedIds = new Set(bookmarks.map((b: any) => b.data.sessionId)); return ( <Schedule sessions={sessions} bookmarkedIds={bookmarkedIds} /> ); }'self'resolves on the server to the authenticated customer's id. You don't need to thread the customer id through your code.Bookmarking a session is one POST:
await repositoryAPI.create('bookmark', { eventId: event.id, sessionId: 'session-id', customerId: 'self', });Workflow on
bookmark.createdadds it to the attendee's calendar feed and (optionally) sends a 15-minute-before reminder push. - 6
Community feed
Posts use the built-in
postcollection scoped to the event. Filter byeventIdand order by date.// src/app/(attendee)/feed/page.tsx import { repositoryAPI } from '@/lib/repository-api'; export default async function FeedPage() { const event = await getCurrentEvent(); const posts = await repositoryAPI.list('post', { filter: { eventId: event.id, state: 'published' }, sort: '-createdate', limit: 50, }); return <Feed posts={posts} />; }A new-post form is just a write:
await repositoryAPI.create('post', { eventId, content: text, attachments: imageUrls, authorId: 'self', state: 'published', });Likes and comments are nested collections (
post-like,post-comment). The community module wires up notification fan-out — when somebody likes your post, a workflow fires a push. - 7
In-app chat
The chat module covers DMs, group rooms, and the event-wide channel. From the Next.js client:
'use client'; import { useEffect } from 'react'; import { io } from 'socket.io-client'; export function EventChat({ eventId, conversationId }: Props) { useEffect(() => { const socket = io(process.env.NEXT_PUBLIC_APPENGINE_URL!, { auth: { token: getToken(), orgId: getOrgId() }, }); socket.emit('join', { conversationId }); socket.on('message', (msg) => appendMessage(msg)); return () => { socket.disconnect(); }; }, [conversationId]); // ... }Or — for a turnkey UI — drop in the chat-client widget. See Embed the chat widget. For an event the widget points at a special
eventId-scoped config and the conversations show up filtered. - 8
Networking — find people
The participants endpoint returns everyone with a role on the event (speakers, sponsors, attendees if set to public). Filter by interests for matchmaking.
const speakers = await eventsAPI.getEventParticipants(eventId, { role: 'speaker' }); const peopleLikeMe = await eventsAPI.getEventParticipants(eventId, { role: 'attendee', interests: ['typescript', 'distributed-systems'], });A "request to connect" is a write to
connectionwithstate: 'pending'; accepting flips it toaccepted. The chat gateway lets connected users DM. - 9
Check-in
At the door, an admin user opens a check-in screen. They scan the attendee's QR (the ticket code) and post:
await client.processRequest('post', `client/events/${eventId}/check-in`, { ticketCode: scannedCode, });AppEngine validates the code, marks the ticket
state: 'used', and writes acheck-in-eventfor the audit trail. If the code is reused, the second call fails with 409. - 10
Push notifications
Register the device on first login:
await client.processRequest('post', 'data/push-device', { data: { token: fcmToken, platform: 'web', // or 'ios' / 'android' customerId: 'self', eventId: currentEventId, }, });Workflows on
event.starting-soon,session.starting,post.replied-tosend notifications via AppEngine's push module — no client code beyond registration.
Mobile mirror
event_app (Flutter) is the canonical mobile reference. Screens map directly to web routes:
| Flutter screen | Web route |
|---|---|
customer_home_screen.dart | / (event landing) |
events_screen.dart | /events |
event_detail_screen.dart | /events/[id] |
schedule_screen.dart | /(attendee)/schedule |
session_detail_screen.dart | /(attendee)/schedule/[sessionId] |
feed_screen.dart | /(attendee)/feed |
people_screen.dart | /(attendee)/people |
chat_screen.dart | embedded chat widget |
inbox_screen.dart | /(attendee)/inbox |
ticket_screen.dart | /(attendee)/tickets |
qr_profile_screen.dart | /(attendee)/profile/qr |
Service layer (event_app/lib/services/):
api_service.dart— HTTP clientcommunity_chat_service.dart— WebSocket wrappermedia_upload_service.dart— uploadsactivity_tracking_service.dart— analytics
If you're building both web and mobile, prototype on mobile (the screens are pre-built) and port routes one at a time.
What's next
- For the public marketing site that drives traffic to the event, use dynamic pages from the CMS.
- For paid sessions and per-track upgrades, see Add storefront checkout.
- For an AI-powered "ask the conference" feature, see Add AI features.