Let members host events
Requires: A secret key with
MEMBER_AUTH, and the member's session.
Members who may host events can create them on your site, in the same wizard the community app uses: the same steps, controls and behaviour, with your colours and fonts. The event, its tickets and its listing request land in Cobuntu exactly as if the member had used the app.
What you get:
- the same steps as the app: details, tickets, the listing arrangement, access and settings, plus the ownership choice for members who may create on the community's behalf;
- drafts that autosave a second after each change and can be resumed later;
- the paid-ticket gate: a member with paid tickets connects their own Stripe in the community app's payouts hub, then comes back;
- your own extra steps, appended before the final step, whose answers stay with you.
How it fits together
Browser Your server Cobuntu
EventCreateWizard --fetch--> /api/cobuntu/* --secret key--> /api/v1/communities/:tag/me/*
(proxy transport) (createEventWizardProxy) + member sessionThe member routes need a secret key with MEMBER_AUTH, and a secret key
must never reach the browser. So the wizard runs in the browser on a proxy
transport that calls your own server, and your server forwards a fixed list of
routes to Cobuntu with the key and the member's session token. Nothing else
goes through.
1. Install
npm install @cobuntu/embed
npm install github:cobuntuHello/cobuntu-event-management-ui@cobuntu/event-management-ui ships TypeScript source. On Next.js, add it and
its shared dependency to transpilePackages:
// next.config.ts
export default {
transpilePackages: ["@cobuntu/event-management-ui", "@cobuntu/management-ui-shared"],
};2. Mount the proxy on your server
// app/api/cobuntu/[...path]/route.ts
import { cookies } from "next/headers";
import { createEventWizardProxy } from "@cobuntu/embed/event-wizard";
const proxy = createEventWizardProxy({
apiKey: process.env.COBUNTU_SECRET_KEY!, // secret key with MEMBER_AUTH
community: "your-community",
// The session token your sign-in stored in an httpOnly cookie.
getSessionToken: async () => (await cookies()).get("session")?.value ?? null,
});
async function handle(req: Request, ctx: { params: Promise<{ path: string[] }> }) {
return proxy(req, (await ctx.params).path);
}
export { handle as GET, handle as POST, handle as PUT, handle as DELETE };
export const dynamic = "force-dynamic";The proxy answers 401 INVALID_SESSION without a session and 404 for any
route that is not one of the wizard's. Cobuntu's own status and
{ error, code } body come back unchanged.
3. Gate the page
Mirror the app: signed-out visitors go to sign in, and members who may not host
go back to your events page. coCreation.get() answers both.
// app/events/new/page.tsx (server component)
import { redirect } from "next/navigation";
export default async function NewEventPage() {
const session = await readSession();
if (!session) redirect("/login?next=/events/new");
const coCreation = await cobuntu.as(session).coCreation.get();
if (coCreation.event.state !== "allowed") redirect("/events");
return <HostEvent event={coCreation.event} />;
}The event entry also carries what the wizard needs to describe the
arrangement: memberEventFeeModel, memberEventRequireApproval,
platformFees.events and payments (the community's own Stripe).
4. Render the wizard
"use client";
import "@cobuntu/event-management-ui/event-wizard.css";
import "@cobuntu/event-management-ui/theme.css";
import { EventCreateWizard } from "@cobuntu/event-management-ui";
import { createProxyEventWizardTransport } from "@cobuntu/embed/event-wizard";
const transport = createProxyEventWizardTransport({ baseUrl: "/api/cobuntu" });
const APP = "https://your-community.cobuntu.com";
export function HostEvent({ event, communityId, connectUrl }) {
return (
<EventCreateWizard
community={{
tag: "your-community",
id: communityId,
name: "Your Community",
payments: event.payments,
platformFeeRate: event.platformFees?.events,
memberFeeModel: event.memberEventFeeModel ?? null,
memberRequireApproval: !!event.memberEventRequireApproval,
}}
transport={transport}
navigation={{
hrefs: {
events: "/events",
createAnother: "/events/new",
viewEvent: (slug) => `/events/${slug}`,
manageEvent: (slug) => `${APP}/events/${slug}/manage`,
reviewListing: (slug) => `${APP}/events/${slug}/manage/manage-listings`,
connectStripe: connectUrl, // from me.stripe().connectUrl
howItWorks: `${APP}/selling-here`,
sellingGuide: `${APP}/selling-here`,
fullGuide: `${APP}/hub/selling-on-cobuntu`,
},
onDone: (result) => console.log(result.event.slug, result.extraValues),
}}
theme={{ bgColor: "#f4efe6", textColor: "#1a1a1a", brandColor: "#c8102e" }}
/>
);
}event-wizard.css is the wizard's own stylesheet, prebuilt and scoped under the
wizard's root class, with no global reset: it does not restyle the rest of your
site and needs no Tailwind. theme.css maps the wizard's palette onto the
theme variables below.
Portalled layers (the description, location, tags and ticket editors) mount
inside the wizard by default (portal="inline"), so the stylesheet and your
theme reach them too.
Theme variables
Pass the theme prop, or set the variables yourself on .cbt-event-wizard.
theme prop | CSS variable | Used for |
|---|---|---|
textColor | --text-color | body text; every grey in the form is mixed from it |
bgColor | --bg-color | page background; footers and modal panels mix toward it |
brandColor | --brand-color | progress bar, primary buttons, selected cards, switches |
brandContrast | --brand-contrast | text on brand-coloured fills (default white) |
primaryButtonBg | --primary-btn-bg | primary button fill (defaults to the brand colour) |
buttonRadius | --button-radius | buttons, cards and rows (0 for square corners) |
cardRadius | --card-radius | the first-time explainer panel |
headingFont | --heading-font | page and step headings |
bodyFont | --body-font | everything else |
The defaults sit at zero specificity, so any rule of yours wins. Layout, spacing and behaviour are not themeable: they stay the app's.
Extra steps
Ask your own questions after Cobuntu's steps and before the final one. They
show on the progress rail, block Next until valid, and their answers reach
onDone as result.extraValues[stepId]. Cobuntu does not store them: send
them wherever you keep such answers today.
const extraSteps = [
{
id: "about-you",
label: "About you",
title: "About you",
subtitle: "So we can reach you about the event.",
render: ({ value = {}, onChange, showErrors }) => (
<label>
Phone
<input value={value.phone ?? ""} onChange={(e) => onChange({ ...value, phone: e.target.value })} />
{showErrors && !value.phone && <span>Required</span>}
</label>
),
validate: (value) => (value?.phone ? null : "Add a phone number"),
},
];
<EventCreateWizard extraSteps={extraSteps} navigation={{ ..., onDone: (r) => saveAnswers(r.event, r.extraValues) }} />onDone runs once the event exists, before the confirmation step shows, with
the event (id, slug, name), the listing outcome, the button pressed and
the ownership.
Your events on the profile
Once members can create events, give them somewhere to find their drafts. The
proxy also forwards GET me/profile/events, the member's own hosted events with
their listing state and drafts, so a client component can fetch
/api/cobuntu/me/profile/events and render ProfileEvents from
@cobuntu/embed-react. A draft card should open a sheet with Continue setup
(/events/new?draft=<id>, where the wizard page passes draftId on to
EventCreateWizard) and Delete (DELETE /api/cobuntu/me/drafts/event/<id>).
Any other card offers View and Manage. See Profile events.
Transport reference
@cobuntu/embed/event-wizard is safe to import in the browser: it never loads
the server client.
| Export | Where | What |
|---|---|---|
createProxyEventWizardTransport({ baseUrl, headers?, fetch? }) | browser | the wizard's transport over your proxy |
createEventWizardProxy({ apiKey, community, getSessionToken, baseUrl?, maxBodyBytes?, fetch? }) | server | (request, path) => Response, forwarding only the routes below |
createEventWizardTransport(client.as(session)) | server | the same transport over the typed client |
EVENT_WIZARD_ROUTES, isEventWizardRoute(method, path) | either | the forwarded routes |
Forwarded routes, relative to /api/v1/communities/:tag/:
| Route | Transport call |
|---|---|
GET me/drafts?kind=event | listDrafts |
GET / PUT / DELETE me/drafts/event/:id | getDraft, saveDraft, deleteDraft |
GET me/packages?productType=EVENT | myPackages |
GET me/stripe | stripeStatus |
GET me/permissions | permissions |
GET categories?type=EVENT | categories |
GET segments/public | tiers |
GET me/tags?q=, POST me/tags | searchTags, createTag |
POST me/events | createEvent |
PUT me/events/:id/donations | saveDonations |
GET me/profile/events | none: read it from your profile page (see below) |
Read failures resolve to safe fallbacks (no drafts, no categories, Stripe not
connected). A draft save answers { ok: false, status: 409 } once the draft
has become an event, and the wizard stops autosaving. A failed create reports
Cobuntu's status, code and message, for example 400 STRIPE_REQUIRED.
The banner travels inside the create call as a data URL, so the proxy accepts
bodies up to 12 MB by default (maxBodyBytes). Raise your platform's request
size limit to match if it is lower.
The same routes are on the typed client for your own server code:
me.drafts.list/get/save/delete, me.packages(), me.stripe(),
me.permissions(), me.events.create(), me.events.saveDonations(),
me.tags.search/create and categories(). See the client
reference.