Consero curates invitation-only executive events. I designed and built the platform that replaced fragmented Salesforce pages, manual PDFs, and a legacy mobile app, supporting attendees and sponsors across three brands.
Product Design, Design System, Front-end Development, React Query, Next.js

The Challenge
Attendees expect the product to work like the apps they use every day. Event teams needed to run registration, scheduling, meeting requests, logistics, and agenda production without losing Salesforce as the system of record.
The product had to make complex workflows feel simple, under real constraints:
- 300+ attendees per event, multiple events running at once
- Multiple user types with different permissions and workflows
- Salesforce-backed API responses often exceeding 1 second
- Unreliable Wi-Fi in event venues
- Three branded experiences, one codebase
Mapping the Event Lifecycle
Before designing screens or writing code, I talked to attendees and sponsors and mapped their journeys from start to finish. This helped us see where people needed more context, where the experience could stay simple, and which moments had to feel fast.
Three principles came out of it:
- Wayfinding: Use visual cues to guide attention as the event progresses.
- Feedback: Update immediately, then roll back and explain what happened if something fails.
- Anticipation: Preload the data people are most likely to need next.
System Architecture
Browser requests pass through Next.js route handlers, which call the APEX API using server-held credentials. Salesforce stays behind that boundary, so the UI can behave like a real product instead of a thin wrapper around it.
Requests can fail for several reasons: an expired session, missing permissions, an invalid request body, or an unexpected Salesforce response. Without a shared error model, every screen has to interpret those failures differently, making product states harder to design and test.
return Result.gen(async function* () {
const config = yield* getAuthConfig();
const session = yield* requireUserSessionWithType(rawSession);
const attendeeDetails = yield* Result.await(parseRequestBody(request));
const attendee = yield* Result.await(
postAttendeeDetails({
accessToken: session.accessToken,
config,
eventId,
profileId,
body: attendeeDetails,
}),
);
return Result.ok(attendee);
});Contextual Dashboard
Attendees needed one place to see what to do next. The tasks shown depend on the event timeline, attendee type, completion status, and data from Salesforce. Some actions only become available before or after a specific milestone.
I designed the dashboard around four explicit task states:
UnavailableActiveCompletedEditable
Unavailable tasks have less visual emphasis and show when they will open. Active tasks stand out as the next action to take. Completed tasks confirm progress, while editable tasks let attendees return and update their information when needed.


Metadata-Driven Forms
Each event needs its own mix of forms: registration, profiles, goals, feedback, logistics, and partner requests. The first version of the Form Builder worked, but one component was responsible for nearly everything: rendering, validation, navigation, autosave, mutations, uploads, and every edge case.
I refactored it into a feature-local compound component with a small public API, a view model that manages the workflow, and a component structure that is easy to follow:
<FormBuilder.Root>
<FormBuilder.Sidebar />
<FormBuilder.Main>
<FormBuilder.StepHeader />
<FormBuilder.Form>
<FormBuilder.Fields />
<FormBuilder.Footer />
</FormBuilder.Form>
<FormBuilder.MobileLiaisons />
</FormBuilder.Main>
</FormBuilder.Root>Salesforce metadata maps to reusable field renderers grouped by responsibility: basic inputs, choices, file uploads, session feedback, and partner lists.
The view model manages the active step, dirty sections, autosave, validation, and focus on the first invalid field. React Query handles mutations, rollback, and cache invalidation.
Read the deep dive

Composable Tables
Preference ranking is the most demanding table workflow in the product. It combines large sets of companies or attendees with search, sorting, filters, sticky controls, drafts, and submission. The same table patterns also support check-in lists, itineraries, attendee directories, and session rosters.
I used PreferenceTable to establish a shared structure: root, header, body, and row. Each workflow then adds the behavior it needs: ranking and drafts for preferences, status updates for check-in, and schedule details for itineraries.


Fast Navigation Over Slow APIs
Salesforce-backed requests can take more than a second, and venue Wi-Fi can make them even slower. During a live event, attendees opening their itinerary still expect the product to respond immediately. I couldn’t make every upstream request fast, so I made the most common navigation paths feel fast.
React Query prefetches data for likely next routes before the attendee clicks, using centralized query keys:
queryClient.prefetchQuery({
queryKey: qk.event.attendeeDetails(eventId, profileId),
queryFn: () => fetchAttendeeDetails({ eventId, profileId }),
staleTime: 5 * 60 * 1000,
});When the data is already cached, the next screen can render in roughly 50 milliseconds. Requests that previously took more than a second become invisible on common paths. Repeat visits reuse the same data, while the qk factory keeps cache invalidation consistent.



Spark Design System
Spark is the design system I built to carry the Consero aesthetic across the product without tying components to a single brand.
Semantic tokens define product roles for surfaces, text, borders, icons, focus, and status. Each brand supplies its own values through data-brand-theme, allowing Consero, Eaton Hall, and Quartz to share the same components while keeping distinct identities.
Spark includes more than 75 reusable elements documented across 50 pages. The system builds from small foundations into complete product experiences:
primitivecomponentpatternblockscreen


One Product, Three Themes
Each brand owns its neutral and accent scales, with dedicated light and dark modes, then maps them to tokens like surface-page, text-muted, border-brand, focus, and status-success.
The root sets a single data-brand-theme, while components keep the same markup and reference semantic tokens instead of brand-specific colors.
/* Palette → semantic roles */
[data-brand-theme="quartz"] {
--brand-neutral-0: 255 255 255;
--brand-neutral-500: 30 24 42;
--brand-neutral-900: 12 0 36;
--brand-accent-500: 99 37 255;
--surface-control-selected: var(--brand-accent-500);
--text-on-brand: var(--brand-neutral-0);
--border-brand: var(--brand-accent-500);
--focus: var(--brand-accent-500);
}
/* Mode-specific overrides */
.dark[data-brand-theme="quartz"] {
--surface-row: var(--brand-neutral-900);
--surface-dialog: var(--brand-neutral-500);
--text: var(--brand-neutral-0);
}Consero, Eaton Hall, and Quartz can share the same workflows while still feeling like distinct products. New components inherit the right theme automatically, and brand updates happen once at the token layer instead of throughout the product.

Outcomes
The platform is now the starting point for every event across Consero, Eaton Hall, and Quartz. Attendees use one product for registration, schedules, meeting preferences, and logistics. The legacy mobile app has been retired, along with the PDFs, spreadsheets, and email chains that operations teams previously managed by hand.
4x
Faster event setup. Operations teams launch registration, agendas, and logistics in days, not weeks
8k+
Attendees served across three brands, all on one codebase
80%
Of manual event workflows eliminated. PDF agendas, spreadsheets, and email chains retired
PWA
Installs from the browser to the home screen. The legacy native app is retired, with nothing to download or update