Guides
Events
Let members host events

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 session

The 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 propCSS variableUsed for
textColor--text-colorbody text; every grey in the form is mixed from it
bgColor--bg-colorpage background; footers and modal panels mix toward it
brandColor--brand-colorprogress bar, primary buttons, selected cards, switches
brandContrast--brand-contrasttext on brand-coloured fills (default white)
primaryButtonBg--primary-btn-bgprimary button fill (defaults to the brand colour)
buttonRadius--button-radiusbuttons, cards and rows (0 for square corners)
cardRadius--card-radiusthe first-time explainer panel
headingFont--heading-fontpage and step headings
bodyFont--body-fonteverything 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.

ExportWhereWhat
createProxyEventWizardTransport({ baseUrl, headers?, fetch? })browserthe 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))serverthe same transport over the typed client
EVENT_WIZARD_ROUTES, isEventWizardRoute(method, path)eitherthe forwarded routes

Forwarded routes, relative to /api/v1/communities/:tag/:

RouteTransport call
GET me/drafts?kind=eventlistDrafts
GET / PUT / DELETE me/drafts/event/:idgetDraft, saveDraft, deleteDraft
GET me/packages?productType=EVENTmyPackages
GET me/stripestripeStatus
GET me/permissionspermissions
GET categories?type=EVENTcategories
GET segments/publictiers
GET me/tags?q=, POST me/tagssearchTags, createTag
POST me/eventscreateEvent
PUT me/events/:id/donationssaveDonations
GET me/profile/eventsnone: 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.