Events
Complete schedule, free RSVP, guest lookup and cancellation with explicit sign-in policy limits.
Events
Prepare
Enable events and prepare a published public calendar, a published public/unlisted event with registrationPolicy: rsvp (or approval), and an open occurrence with capacity. This guide uses free direct RSVP intake, not cart, checkout, paid admission or an operator registration endpoint. rsvp normally returns going; approval returns pending; explicit auto-waitlist policy can return waitlisted when full. Show the returned status, not “confirmed” for every success.
For new sign-in integrations, first copy all Customers files and configure the same workspace’s hosted client/origin/redirect and HttpOnly session. Public schedule reads and guest intake do not themselves require customer identity. Browser calls need separate API origin admission.
These examples use the published SDK 0.65.1, Next.js 16.3.8 App Router, React 19.2.4, and Node 24.15.0. Install the SDK in your own app with npm install @01.software/sdk@0.65.1. The examples use force-dynamic; SDK calls run on requests, not at build time. This assumes no cacheComponents or static export. Compile and controlled local transport checks do not certify hosted availability or remote workspace admission.
Implement schedule and free RSVP
Copy these files after the Customers session helper. signupRequired below is an app-owned constant, not a platform event field. With true, this app’s trusted Server Action requires hosted me() readback before registration. Set it to false to allow guests through this app. Invalid presented sessions are sent back to sign-in rather than silently downgraded. No caller-supplied customer or workspace is sent to the SDK.
The platform currently has no server-enforced signupRequired policy. Direct public intake evaluates registration policy, publication/visibility, calendar/occurrence, duplicates and capacity; customer JWT is optional. Requiring sign-in in this app does not prevent another client from calling guest direct intake with the Publishable Key. This recipe does not establish workspace-wide authenticated-only registration.
// app/events/page.tsx
import { createClient } from '@01.software/sdk'
import { registerRsvp } from './actions'
export const dynamic = 'force-dynamic'
export default async function EventsPage() {
const client = createClient({
publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY!,
apiUrl: process.env.SOFTWARE_API_URL,
})
const range = await client.events.getRange({ limit: 10 })
const occurrences = range.docs.filter(
(row) =>
row.status === 'scheduled' &&
['rsvp', 'approval'].includes(row.event.registrationPolicy ?? ''),
)
if (occurrences.length === 0)
return <p>No open RSVP occurrences in this range.</p>
return (
<ul>
{occurrences.map((occurrence) => (
<li key={occurrence.id}>
<h2>{occurrence.event.title ?? 'Untitled event'}</h2>
<p>
{occurrence.isAllDay
? `${occurrence.allDayStartDate} / ${occurrence.allDayEndDateExclusive} (exclusive)`
: occurrence.startsAt}
</p>
<form action={registerRsvp}>
<input
type="hidden"
name="event"
value={String(occurrence.event.id)}
/>
<input
type="hidden"
name="occurrence"
value={String(occurrence.id)}
/>
<input
name="email"
type="email"
required
aria-label="Attendee email"
/>
<button>RSVP</button>
</form>
</li>
))}
</ul>
)
}// app/events/actions.ts
'use server'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { currentCustomer, cookieOptions } from '@/lib/customer'
// App-owned policy. false permits guests; true gates this app's action only.
const signupRequired = true
export async function registerRsvp(form: FormData) {
const { client, customer, hadToken } = await currentCustomer()
if ((signupRequired || hadToken) && !customer) redirect('/api/account/login')
const event = String(form.get('event') ?? '')
const occurrence = String(form.get('occurrence') ?? '')
const email = String(form.get('email') ?? '').trim()
if (!event || !occurrence || !email)
throw new Error('Select an event and enter an email')
const result = await client.events.register({
event,
occurrence,
quantity: 1,
attendee: { email },
})
// Token is a capability: never put it in a URL, log, or public response.
const jar = await cookies()
jar.set('__Host-rsvp-token', result.guestToken, {
...cookieOptions,
expires: new Date(result.guestTokenExpiresAt),
})
redirect('/events/manage')
}// app/events/manage/page.tsx
import { cookies } from 'next/headers'
import { customerClient, cookieOptions } from '@/lib/customer'
import { CancelRsvp } from './cancel-form'
export const dynamic = 'force-dynamic'
export default async function ManageRsvp() {
const token = (await cookies()).get('__Host-rsvp-token')?.value
const registration = token
? (await customerClient().events.getGuestRegistration(token)).registration
: null
async function cancel(): Promise<'canceled'> {
'use server'
const jar = await cookies()
const current = jar.get('__Host-rsvp-token')?.value
if (!current) throw new Error('RSVP token missing')
const result =
await customerClient().events.cancelGuestRegistration(current)
if (result.registration.registrationStatus !== 'canceled')
throw new Error('Cancellation unconfirmed')
// Successful cancellation revokes the guest token; never look it up again.
jar.set('__Host-rsvp-token', '', { ...cookieOptions, maxAge: 0 })
return 'canceled'
}
return (
<main>
<h1>Manage RSVP</h1>
<CancelRsvp
registrationStatus={registration?.registrationStatus ?? null}
cancel={cancel}
/>
</main>
)
}// app/events/manage/cancel-form.tsx
'use client'
import { useActionState } from 'react'
type CancelState = 'ready' | 'canceled' | 'failed'
export function CancelRsvp({
registrationStatus,
cancel,
}: {
registrationStatus: string | null
cancel: () => Promise<'canceled'>
}) {
const [state, submit, pending] = useActionState<CancelState, FormData>(
async () => {
try {
return await cancel()
} catch {
return 'failed'
}
},
'ready',
)
if (state === 'canceled') return <p role="status">RSVP canceled.</p>
if (!registrationStatus)
return (
<p>
No RSVP token in this browser. Make a new reservation or contact
support.
</p>
)
if (!['pending', 'going', 'waitlisted'].includes(registrationStatus))
return <p>RSVP: {registrationStatus}. Contact support for changes.</p>
return (
<form action={submit}>
<p>RSVP: {registrationStatus}</p>
{state === 'failed' && (
<p role="alert">
Cancellation unconfirmed. Contact support before retrying.
</p>
)}
<button disabled={pending}>Cancel RSVP</button>
</form>
)
}Normal, empty, and failure results
Open /events: only scheduled occurrences whose registration policy is rsvp or approval are offered. An empty filtered range is a successful empty state; none/external events and completed occurrences have no RSVP form. These display filters do not replace server admission or reserve capacity. Filtering happens in the app after getRange({ limit: 10 }): an empty filtered first page does not prove the whole calendar has no RSVP occurrences. Paginate before drawing that conclusion. Declined registrations display their status without a Cancel button. For all-day rows, display canonical start/exclusive-end dates as dates; do not reconstruct them by viewer-timezone conversion. Select an occurrence, submit one RSVP, and independently look up /events/manage. Cancel there: only the successful cancellation response with registrationStatus: canceled displays the completion message and removes the guest-token cookie. Do not redirect or query again with that token: cancellation revokes it, so subsequent lookup and cancellation return 401. Reloading shows the no-token state. Registration status before cancellation comes from lookup; independent post-cancel readback requires an authorized workspace operator, not the revoked guest token. The guest token is returned once; the server keeps its hash. This example retains only one RSVP capability in a Secure HttpOnly cookie: a later RSVP replaces it. Losing it requires the operator’s support flow, not listing private registrations. The guest lookup response can contain attendee PII; render only the status and never dump the response.
A missing cookie displays the empty management state. If an operator has canceled a registration or occurrence, its remaining cookie can also lead to token rejection. In the management read error UI, show “RSVP access unavailable. Contact the event organizer to verify the status and reset this browser’s RSVP cookie.” Repeating the same rejected lookup cannot restore access, and cookie removal alone does not prove cancellation. An invalid/expired guest token, closed occurrence, full capacity without waitlisting, duplicate registration, feature denial or network failure must remain an error. A foreign/invalid customer JWT that makes me() return null redirects this app’s action to sign-in; it is never silently treated as guest intake. Add a read retry boundary for pages; provide action failure UI without automatically resubmitting a registration or cancellation. A registration write can commit even if the response is lost; investigate/read back before retrying. A failed cancel, including any 401, must not display a canceled success or clear the cookie as if cancellation were confirmed. If a response is lost, contact an authorized workspace operator to verify state; token rejection alone does not prove cancellation.
Starter compatibility and local checks
The published create 0.2.5 → init 0.14.3 flow tested in Quickstart includes Events RSVP 0.1.21 with SDK 0.65.0. That starter is guest-first/free; its optional customer-account surface uses direct legacy auth, not this new hosted recipe. It has no server signupRequired enforcement. Its guest links may carry capability tokens; avoid logging/referrer leakage and prefer the cookie pattern here for new apps. Do not introduce checkout/payment keys to complete free RSVP. A mock demo or controlled local transport proves neither live admission nor hosted deployment.
Verify guest-first registration → lookup → cancel, then this app’s signed-in and signed-out paths with signupRequired: true. Also verify a separate guest direct call remains possible when intake is public. Server-enforced signupRequired is unavailable, not a passing test. Remote test-workspace readback/denial/cleanup and hosted end-to-end checks remain pending until run against the same staging SHA.
Next Actions
Use Content Operations for calendar ownership and occurrence cancellation, SDK for paid admission availability, and Deploy before launch. Availability is a hint, not a seat reservation; paid ticket purchase follows the existing Ecommerce flow and is outside this free RSVP task.
The next init release retires the separate events-rsvp starter. Use
--template events for new free RSVP/approval or paid-event apps; the retired
name fails with exit code 1 and a migration hint. Events still requires Events +
Ecommerce by default. This leaves the pinned historical observation above and
existing generated apps unchanged. See Build Options
for the next-release roster and publication caveat.