Platform administration
Spaces, clubs and owners managed by platform admins.
Platform admins run the Champagne instance itself. They create a space for each customer,
add its clubs and choose its owner. The role is a flag on the staff account
(staffViewer.isPlatformAdmin); it gives no access to the data of any club. A platform admin
who also works in a club needs club access like any other staff member.
The demo seed creates the platform admin superadmin@example.com, who has no club. Platform
admins then give the role to others with grantPlatformAdmin.
Permissions
| Operation | Permission |
|---|---|
platformSpaces | Platform admin |
platformAdmins, platformActivity, grantPlatformAdmin, revokePlatformAdmin | Platform admin |
createPlatformSpace, createPlatformClub, setPlatformSpaceOwner, cancelPlatformOwnerInvitation | Platform admin |
renamePlatformSpace, updatePlatformClub, setClubStatus, setClubFeatures, setClubPhoneCodeCap | Platform admin |
setPlatformSpaceArchived | Platform admin |
| platformApiTokens, createPlatformApiToken, setPlatformApiTokenBlocked, deletePlatformApiToken | Platform admin |
Club admins and space owners are refused with UNAUTHENTICATED. A
platform API token is limited to platformSpaces,
createPlatformClub, updatePlatformClub, setClubStatus and setClubFeatures.
List spaces
query PlatformSpaces {
platformSpaces {
id
name
owner {
id
name
email
}
pendingOwner {
email
expiresAt
expired
removesPreviousOwner
}
clubs {
id
name
timezone
hostedSlug
status
disabledFeatures
}
}
}Spaces are ordered by name, and their clubs by name. owner is null until an invited owner
accepts. pendingOwner is the owner invitation waiting to be accepted, for a new space or
a transfer. expiresAt is when its link stops working, 7 days after it was sent, and
expired turns true from then on: the invitation must then be sent again.
removesPreviousOwner tells whether the current owner loses their access once it is
accepted.
Create a space
mutation CreatePlatformSpace($input: CreatePlatformSpaceInput!) {
createPlatformSpace(input: $input) {
id
owner {
email
}
pendingOwner {
email
}
}
}{
"input": {
"name": "Riviera group",
"ownerEmail": "owner@riviera.example",
"clubs": [{ "name": "Riviera Nice", "timezone": "Europe/Paris", "hostedSlug": "riviera-nice" }]
}
}The space and its clubs are created in one transaction. Each club starts with the default settings and onboarding of any new club.
- If
ownerEmailmatches an existing staff account, that account becomes owner at once and admin of every club in the space. - Otherwise the email receives an invitation to the back office's
/invitationpage. Accepting it creates the account (or reuses one created since), makes it owner and admin of every club in the space. The invitation never appears in the space's team list.
If the email cannot be delivered, the space stays created, the invitation is withdrawn, and
the mutation fails with a message saying so. Send the invitation again with
setPlatformSpaceOwner.
Add a club
mutation CreatePlatformClub($spaceId: ID!, $input: PlatformClubInput!) {
createPlatformClub(spaceId: $spaceId, input: $input) {
clubs {
id
name
}
}
}The owner, if there is one, becomes admin of the new club. Other staff get access from the team page as usual.
Change the owner
mutation SetPlatformSpaceOwner($spaceId: ID!, $email: String!, $removePreviousOwner: Boolean) {
setPlatformSpaceOwner(
spaceId: $spaceId
email: $email
removePreviousOwner: $removePreviousOwner
) {
owner {
email
}
pendingOwner {
email
}
}
}The same rule applies: an existing account takes over at once, anyone else is invited. The previous owner stays owner until the invitation is accepted. Calling it again with the pending email sends a fresh invitation and withdraws the previous one.
removePreviousOwner defaults to true: when the new owner takes over (at once for an
existing account, on acceptance for an invitation), the previous owner loses every club
access, reservation assignment and their place in the space, as if removed from the team.
Pass false to keep them as admin of the clubs.
Cancel an owner invitation
mutation CancelPlatformOwnerInvitation($spaceId: ID!) {
cancelPlatformOwnerInvitation(spaceId: $spaceId) {
pendingOwner {
email
}
}
}Withdraws the owner invitation waiting on the space; its link stops working at once. The current owner, if any, is unchanged.
Suspend a club
mutation SetClubStatus($clubId: ID!, $status: ClubStatus!) {
setClubStatus(clubId: $clubId, status: $status) {
clubs {
id
status
}
}
}status is active or suspended. Suspending keeps every record of the club, but from the
next request:
- the club disappears from the
clubIdsandclubRolesof its staff sessions, so its operations are refused, and staff whose only club it was see the no-club page; - member sessions of the club no longer resolve, and
loginMemberis refused withThis club is suspended. Contact the club for more information.; - the club leaves
clubDirectory, andhostedClubreturns null for its hosted address; - it leaves
externalBookingClubs,externalReservationCatalogreturns null, its availability is empty andcreateExternalReservationis refused; onboarding,onboardingsandhostedOnboardingreturn nothing,startApplicationreturnsThe onboarding is unavailable., application recovery answersThis club could not be found., and applicant tokens stop granting access to its applications;plansreturns no plan for it, andmembershipOffersandaddonOffersreportMerchant payments or catalog are not ready.- background jobs schedule no reservation or renewal reminder for it, skip its notification emails and promote no one from its waitlists; reservation confirmations, application decisions and application reminders wait and go out once the club is reactivated. Refunds, deposit refunds and their emails continue.
Setting active again restores every access, including existing sessions that have not
expired.
The people of a closed club are told why it is gone:
staffViewer.closedClubslists the clubs the account works in that are suspended or archived, with theirnameandstatus. The back office shows them on its no-club page.- The public
closedClub(clubId)query returns the club'snameandstatuswhen it is suspended or archived, and null otherwise. The member sign-in page asks it for a remembered or requested club thatclubDirectoryno longer lists.
query ClosedClub($clubId: ID!) {
closedClub(clubId: $clubId) {
name
status
}
}Archive a club or a space
setClubStatus with archived closes the club exactly like a suspension; the only
difference is for the people involved: loginMember answers This club is closed. and the
platform page hides archived clubs until its archives are shown. Restore a club with
status: active.
mutation SetPlatformSpaceArchived($spaceId: ID!, $archived: Boolean!) {
setPlatformSpaceArchived(spaceId: $spaceId, archived: $archived) {
archived
clubs {
id
status
}
}
}Archiving a space sets archived and archives every club of the space, suspended ones
included. While the space is archived, createPlatformClub and setClubStatus with any
status other than archived are refused with This space is archived. Restore it first.
Restoring the space puts each club back in the status it had when the space was archived: a
suspended club stays suspended, and a club archived before the space, or archived with
setClubStatus while the space was archived, stays archived.
Phone code limit
mutation SetClubPhoneCodeCap($clubId: ID!, $perHour: Int) {
setClubPhoneCodeCap(clubId: $clubId, perHour: $perHour) {
clubs {
id
phoneCodesPerHour
}
}
}phoneCodesPerHour is the club's own hourly cap on application phone verification codes,
between 1 and 1,000,000, for example raised for an opening day. Null, the default, makes the
club follow the deployment's per-club cap (SMS_HOURLY_CAP_CLUB, 100 unless configured). Other
values are refused with BAD_USER_INPUT. The per-number, per-connection and platform caps and
the anti-bot check still apply. The change is recorded as platform.club_phone_cap_changed.
Turn modules off
mutation SetClubFeatures($clubId: ID!, $disabledFeatures: [ClubFeature!]!) {
setClubFeatures(clubId: $clubId, disabledFeatures: $disabledFeatures) {
clubs {
id
disabledFeatures
}
}
}Every module is on for a new club. disabledFeatures replaces the list of modules turned
off, returned in the order below whatever the order sent. A module turned off removes the
staff permissions it covers for every role, club admins included: its back-office sections
disappear and its staff operations are refused with UNAUTHENTICATED. Member-facing
operations, such as booking or chatting from the member area, are not affected, except for
billing (see Clubs with billing turned off).
| Module | Staff permissions removed | Sections hidden |
|---|---|---|
reservations | reservations_manage, attendance_manage, chats_manage, planning_read | Reservations, Club calendar, My reservations |
availability | availability_manage | My availability |
customer_chats | customer_chats | Chats, unless reservation messages remain |
housekeeping | housekeeping_manage | Housekeeping |
newsletters | newsletters_manage | Newsletters |
applications | applications_manage | Applications |
billing | billing_manage | Finance |
Reservation categories and resources are configured with catalog_manage, which also covers
plans, so these operations stay available to admins when reservations is off; only the
Reservations section is hidden.
Staff sessions read the modules on every request: staffViewer.clubRoles returns
disabledFeatures for each club, and its permissions no longer list the removed ones.
Clubs with billing turned off
billing also covers members: a club with it off charges nothing. Club.billingEnabled is
false for that club in clubSettings, hostedClub, clubDirectory and a person's club,
so both apps hide prices, credits, top-ups, plans and payment steps.
Every operation that would charge money or credits for the club is refused with
BAD_USER_INPUT and the message Payments are turned off for this club.:
| Area | Operations |
|---|---|
| Memberships | createMembershipCheckout, createApplicationCheckout, createPaymentMethodPortal |
| Credits | createCreditCheckout, configureCredits, and every credit debit: payReservationCredits, payAddonCredits, reservation edits that cost more, reportProductConsumption |
| Reservations | createReservationCheckout |
| Catalog | createPlan, updatePlan, splitPlan |
| Merchants | createMerchant, selectMerchant, merchantOnboarding, prepareMerchantCatalog |
| Onboardings | saveOnboarding with a PLAN_SELECTION or PLAN_PAYMENT step (returned in error) |
Bookings stay open and are free: resource prices, plan prices, deposits and product prices
count as zero, so new reservations are confirmed without payment, including online bookings.
Onboarding reads (onboarding, onboardings, hostedOnboarding) leave out plan selection and
payment steps, and new applications start without them. Turning billing off with
setClubFeatures also removes the plan steps that open applications have not reached yet, in
the same transaction; an accepted application left with no remaining step is marked complete. A club created with billing off gets a
default onboarding without them.
Turning billing off keeps existing data: invoices, credit balances and history, paid memberships, paid reservations and stored prices are not changed. Refunds of earlier payments still work, so a cancellation or deposit refund returns what the member paid. Stripe webhooks still finalize a payment started before billing was turned off. Turning billing back on restores prices and steps as they were stored.
Platform admins
query PlatformAdmins {
platformAdmins {
id
name
email
}
}
mutation GrantPlatformAdmin($email: String!, $name: String!) {
grantPlatformAdmin(email: $email, name: $name) {
id
}
}
mutation RevokePlatformAdmin($userId: ID!) {
revokePlatformAdmin(userId: $userId) {
id
}
}platformAdmins lists the accounts with the role, ordered by name.
grantPlatformAdmin gives the role to the staff account using email, and name is then
ignored. When no account uses it, a new account is created with name (1–100 characters)
and an unusable password, and the email receives a link to the back office's
/reset-password page to choose one, valid for 7 days. If that email cannot be delivered,
the new account is deleted and the mutation fails with
The setup email could not be sent, so no account was created. Try again.
revokePlatformAdmin removes the role and keeps the account and its club access. Removing
your own role is refused with You cannot remove your own platform access., so the platform
always keeps at least one admin.
Platform history
query PlatformActivity($page: Int!) {
platformActivity(page: $page) {
items {
occurredAt
action
actorName
subjectType
subjectLabel
changes {
field
before
after
}
}
hasMore
}
}Every platform mutation records an entry, newest first, 20 per page. action is one of
platform.space_created, platform.space_renamed, platform.space_archived,
platform.space_restored, platform.club_created, platform.club_updated,
platform.club_status_changed, platform.club_modules_changed,
platform.club_phone_cap_changed, platform.owner_changed,
platform.owner_invited, platform.owner_invitation_canceled, platform.admin_granted,
platform.admin_revoked, platform.api_token_created, platform.api_token_blocked,
platform.api_token_unblocked or platform.api_token_deleted. subjectType is space, club or staff, and subjectLabel is
its name at the time.
changes lists the fields that changed: owner (emails), space (for a new club), name,
timezone, hostedSlug, status and disabledFeatures (comma-separated module ids).
These entries belong to no club or space, so they never appear in a club's or a staff
member's history.
Validation
| Field | Rule |
|---|---|
name | Space and club names hold 1 to 100 characters after trimming. |
ownerEmail | A valid email address, compared ignoring case. |
clubs | 1 to 20 clubs when creating a space. |
timezone | An IANA timezone, such as Europe/Paris. |
hostedSlug | Optional; 3 to 63 lowercase letters, digits or hyphens, unique per club. |
Club names are saved with an uppercase first letter, so riviera Nice becomes Riviera Nice.
Names are compared ignoring case: no two spaces share a name, archived ones included, and no two
clubs of a space share a name.
Invalid input is refused with BAD_USER_INPUT. A hosted address already used by another club is
refused with the message This address is already used by another club., a taken space name
with Another space already has this name., and a club name taken in its space with
Another club of this space already has this name.