Champagne

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

OperationPermission
platformSpacesPlatform admin
platformAdmins, platformActivity, grantPlatformAdmin, revokePlatformAdminPlatform admin
createPlatformSpace, createPlatformClub, setPlatformSpaceOwner, cancelPlatformOwnerInvitationPlatform admin
renamePlatformSpace, updatePlatformClub, setClubStatus, setClubFeatures, setClubPhoneCodeCapPlatform admin
setPlatformSpaceArchivedPlatform 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 ownerEmail matches 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 /invitation page. 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 clubIds and clubRoles of 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 loginMember is refused with This club is suspended. Contact the club for more information.;
  • the club leaves clubDirectory, and hostedClub returns null for its hosted address;
  • it leaves externalBookingClubs, externalReservationCatalog returns null, its availability is empty and createExternalReservation is refused;
  • onboarding, onboardings and hostedOnboarding return nothing, startApplication returns The onboarding is unavailable., application recovery answers This club could not be found., and applicant tokens stop granting access to its applications;
  • plans returns no plan for it, and membershipOffers and addonOffers report Merchant 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.closedClubs lists the clubs the account works in that are suspended or archived, with their name and status. The back office shows them on its no-club page.
  • The public closedClub(clubId) query returns the club's name and status when it is suspended or archived, and null otherwise. The member sign-in page asks it for a remembered or requested club that clubDirectory no 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).

ModuleStaff permissions removedSections hidden
reservationsreservations_manage, attendance_manage, chats_manage, planning_readReservations, Club calendar, My reservations
availabilityavailability_manageMy availability
customer_chatscustomer_chatsChats, unless reservation messages remain
housekeepinghousekeeping_manageHousekeeping
newslettersnewsletters_manageNewsletters
applicationsapplications_manageApplications
billingbilling_manageFinance

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.:

AreaOperations
MembershipscreateMembershipCheckout, createApplicationCheckout, createPaymentMethodPortal
CreditscreateCreditCheckout, configureCredits, and every credit debit: payReservationCredits, payAddonCredits, reservation edits that cost more, reportProductConsumption
ReservationscreateReservationCheckout
CatalogcreatePlan, updatePlan, splitPlan
MerchantscreateMerchant, selectMerchant, merchantOnboarding, prepareMerchantCatalog
OnboardingssaveOnboarding 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

FieldRule
nameSpace and club names hold 1 to 100 characters after trimming.
ownerEmailA valid email address, compared ignoring case.
clubs1 to 20 clubs when creating a space.
timezoneAn IANA timezone, such as Europe/Paris.
hostedSlugOptional; 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.

Sur cette page