github HiEventsDev/Hi.Events v2.0.0-rc.3

latest release: v1.11.2-beta
pre-release5 hours ago

Hi.Events v2.0.0-rc.3

Important

Upgrading a self-hosted install from v1.x or an earlier release candidate? Back up your database, then pull and migrate. See Upgrading.

The third release candidate of v2. Features marked (Enterprise) need a licence for production use (see Enterprise features and licensing). Everything else is free to self-host.

What's new

Selling

  • Reserved seating (Enterprise): build seat maps in a visual designer and let buyers choose their own seats, with best available, wheelchair spaces and companion seats.
  • Box office (Enterprise): sell and check people in at the door from any browser. Take cash, card on Stripe Terminal readers or comps, with PIN sign-in for staff.
  • Product purchases view: see who bought each product, with search, totals and an export, and filter orders by product.
  • Refunds on manual orders: paid orders you created by hand can now be refunded.
  • Grouped ticket emails: each email address gets one email with all of its tickets. Before, only the first ticket for a shared address was sent.
  • Invoice PDFs support Chinese and Japanese, and are much smaller.

Event pages and embeds

  • Lineup and schedule: speaker and artist profiles you reuse across events, stages, billing tiers and a drag-and-drop timetable, shown on the event page.
  • Run sheets: import people and slots from CSV or XLSX, export to XLSX, or print.
  • Customisable homepage sections: reorder, hide and rename the sections of event and organizer pages, and add text, gallery, FAQ, video and sponsor sections.
  • New embeds: a button that opens tickets in a pop-up, a lineup, a schedule and an organizer events list.
  • Saved widgets: change an embedded widget without touching your site, and see its views, orders and revenue.
  • New widget editor with a live preview and install guides for HTML, WordPress, Webflow, Squarespace, Wix, Shopify, Framer and React/Next.js.
  • More widget options: show only some products or categories, pin a date, apply a promo code or set the language. Affiliate, promo code and UTM parameters on your page carry through to checkout.

Teams and sign-in

  • Team access controls: a read-only Viewer role, per-member switches for financials and refunds, and members limited to specific organizers or events.
  • Two-factor authentication with an authenticator app, recovery codes and trusted devices. Admins can require it for everyone on the account.
  • Social login with Google, Microsoft or your own OpenID Connect provider (Okta, Keycloak, Authentik and others).
  • Enterprise single sign-on (Enterprise): connect an account to its own identity provider, verify your email domains and require SSO for your team.

API and integrations

  • API keys for Zapier, CRMs and your own scripts: read-only or read and write, never more access than your own, with an expiry you choose.
  • In-app API reference under Settings → API keys, with error codes, rate limits and a backward-compatibility promise.

Around the app

  • One settings area at /settings for your profile, security, team, API keys and account, with an unsaved-changes bar.
  • Redesigned event creation, homepage and ticket designers, organizer and event homepages, products table and date picker. Modals go full screen on phones.
  • First day of the week setting for organizers, used by every calendar (thanks @cornelisonc).
  • Japanese translation (thanks @Tt0x).

Security and self-hosting

  • Security fixes for cross-site request forgery, cross-origin API reads, stored XSS in email previews and more, affecting earlier self-hosted versions. See Security fixes.
  • Spam and abuse protection: rate limits on sign-up, login, orders and promo codes, optional Cloudflare Turnstile, and stricter checks on names.
  • Superadmin dashboard rebuilt, with platform commission, top events and accounts per currency, recent signups and a "needs attention" row.
  • Email delivery tracking: every sent email is recorded, and with Postmark you can see deliveries, bounces, opens and clicks.

Upgrading

Hi.Events Cloud is upgraded for you. On a self-hosted install, back up, pull and migrate. New settings are optional and only needed for the features that use them.

From v1.x

Go straight to this release: you don't need to install rc.1 or rc.2 first. v2 needs PHP 8.3 or higher with the intl and gd extensions, and Node 22 to build the frontend. The Docker images already include them.

  1. Back up your database. If something goes wrong, restore it and switch back to your previous version.
    docker compose exec -T postgres pg_dump -U postgres -F c hi-events > hievents-backup.dump
  2. Pull and migrate.
    • Docker from a clone of this repo (the README install): git fetch --tags && git checkout v2.0.0-rc.3, then docker compose up -d --build in docker/all-in-one. Migrations run before the app starts.
    • Images from Docker Hub: release candidates aren't tagged latest, so set your image tags to v2.0.0-rc.3, then run docker compose pull && docker compose up -d. The all-in-one image migrates on start. With separate backend and frontend images, run docker compose run --rm backend php artisan migrate --force before up -d.
    • Manual install: pull the code, run composer install --no-dev --optimize-autoloader and php artisan migrate --force in backend/, build the frontend with yarn install --frozen-lockfile && yarn build (yarn, not npm), then restart your app server and queue workers.
  3. Add the new variables for any features you want, and check the After upgrading list.

Migrations take seconds on most installs and a few minutes with hundreds of thousands of attendees. On a busy site, stop the app before the backup (leave the database running) and let any separate queue worker empty its queue, so the backup holds every order. A restore loses anything written after the backup, so decide whether to roll back before the upgraded site takes new orders.

What changes for v1 installs:

  • Each existing event becomes a single-date event. Buyers see no difference.
  • Stripe Connect and VAT settings move from the account to each organizer. Existing connections carry over, so nobody has to reconnect.
  • Every event gets a Default check-in list with a public link that shows only attendee names and check-in status. Check-in lists tied to one date become all-dates lists.
  • API integrations: some endpoints and fields changed. See Breaking changes below and in the rc.1 notes.

From rc.1 or rc.2

Follow the same steps. Requirements haven't changed since rc.1.

After upgrading

  • CORS. The API now only answers the site in APP_FRONTEND_URL (and its www. or bare-domain counterpart) by default. If the dashboard loads but API calls fail with CORS errors, fix APP_FRONTEND_URL or list your origins in CORS_ALLOWED_ORIGINS. The all-in-one image isn't affected.
  • Proxies. Behind more than one proxy (Cloudflare in front of nginx, for example), list every hop in APP_TRUSTED_PROXIES, such as 127.0.0.1,172.16.0.0/12,cloudflare. Otherwise every buyer shares one rate limit.
  • All-in-one docker-compose.yml. It only passes the variables it lists. If you keep your own copy, bring over the new environment: lines from the latest file, including DB_CONNECTION=pgsql. APP_PLATFORM_SUPPORT_EMAIL, API_DOCS_ENABLED, OCCURRENCES_QUEUE_NAME, POSTMARK_WEBHOOK_SECRET, AWS_SESSION_TOKEN and the spam check settings aren't listed, so add those yourself if you use them.
  • Scheduler and queue worker. Both need to be running (the all-in-one image runs them). They handle account deletion, recurring dates, SSO domain checks and API key expiry.
  • Stripe webhooks should send payment_intent.succeeded, payment_intent.payment_failed, charge.succeeded, charge.updated, charge.refunded, refund.created, refund.updated, account.updated, payout.paid, payout.updated and, for box office card readers, terminal.reader.action_failed.
  • Several app servers? Use a shared cache such as Redis. 2FA, SSO and box office sign-ins are kept in the cache.

If a migration fails, the all-in-one container exits rather than start half-migrated, which looks like a restart loop. docker compose logs shows why. Fix the cause and run it again. If a migration stopped on a lock timeout, first drop any half-built index listed by SELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid;.

New environment variables

All optional. Add one only if you use the feature it controls.

Variable Default What it does
APP_LICENCE_KEY unset Unlocks Enterprise features. development unlocks them for testing.
SSO_* unset Social login with Google, Microsoft or an OpenID Connect provider. See Single sign-on.
TURNSTILE_SECRET_KEY / VITE_TURNSTILE_SITE_KEY unset Cloudflare Turnstile on sign-up. Set both or neither.
APP_TRUSTED_PROXIES * Proxies allowed to set X-Forwarded-For. cloudflare adds Cloudflare's ranges.
APP_SSR_SHARED_SECRET unset Set on backend and frontend when the SSR server can't be a trusted proxy.
APP_REGISTER_RATE_LIMIT_PER_HOUR, APP_PUBLIC_ORDER_RATE_LIMIT_PER_MINUTE, APP_PUBLIC_PROMO_CODE_RATE_LIMIT_PER_MINUTE, APP_PUBLIC_PAYMENT_INTENT_RATE_LIMIT_PER_HOUR 10, 60, 10, 120 Per-IP limits on sign-ups, orders, promo code lookups and card payments.
APP_SEAT_MAP_MAX_SEATS 3000 Largest seat map that can be saved.
VITE_COOKIE_CONSENT_ENABLED, VITE_COOKIE_CONSENT_DOMAIN, VITE_COOKIE_CONSENT_TEXT false Cookie consent banner.
APP_EVENT_SPAM_CHECK_*, ANTHROPIC_API_KEY off Checks newly published events for spam with Claude and holds suspicious ones for review.
GOOGLE_MAPS_API_KEY unset Address autocomplete.
API_DOCS_ENABLED false Interactive API docs at /docs/api.
OCCURRENCES_QUEUE_NAME unset A separate queue for generating recurring dates. Add it to your worker's --queue= list.
POSTMARK_WEBHOOK_SECRET unset Postmark delivery tracking.
AWS_SESSION_TOKEN unset Session token for SES credentials.

Also set APP_PLATFORM_SUPPORT_EMAIL to a real address, since the public /compliance endpoint now shows it.


In detail

Reserved seating (Enterprise)

Sell specific seats instead of quantities.

  • Seat maps are built per organizer in a visual designer: rows and blocks (straight or curved, with aisles), tables, standing zones, a stage, labels, multiple areas, and seat numbering options. Start from scratch or from a template. Mark seats as wheelchair spaces, add notes, and group seats into price bands.
  • Attach a map to an event and link each price band to one or more ticket types. The event gets its own copy of the map, so later edits to the venue map are reviewed and synced, never applied silently. Seats that have been sold can't be removed or unlinked.
  • Buyers pick seats on an inline map, a full-screen picker, or an accessible list view that works with the keyboard. "Best available" finds adjacent seats. Standing zones only ask how many. Companion seats can only be bought together with a wheelchair space.
  • Seat rules per event: a maximum number of seats per order, a rule that refuses to leave single empty seats stranded, and whether buyers may change their own seat later.
  • Holds use the normal checkout reservation. A seat taken by someone else while choosing is reported and can be replaced. If a hold runs out, the seat goes back on sale; a payment that lands afterwards still completes if nobody took the seat, and is refunded if someone did.
  • Recurring events use one map with availability per date.
  • For organizers: a live seat sales map, holding seats back on one or several dates, moving an attendee to another seat, adding an attendee straight into a chosen seat, a seat sales report per price band, and printable sales and house maps.
  • Seats show everywhere a ticket does: tickets, order and ticket emails (and a {{ order.seats }} Liquid token), checkout summaries, check-in search and scans, the attendees table and the attendee export.

Seated tickets don't offer a waitlist. Maps are limited to 20 price bands and, by default, 3,000 seats (APP_SEAT_MAP_MAX_SEATS).

Box office (Enterprise)

Sell and check people in at the door from any browser.

  • Every event gets a default box office, and you can add more, for example one per entrance. Each has its own URL, a 6-digit PIN shown once when it's created or reset, optional activation and expiry dates, and can be limited to specific products or a single date. A box office without a PIN can't be opened by staff.
  • Staff sign in with the PIN. Five wrong PINs lock the device out for 15 minutes, and resetting the PIN ends every open staff session. A signed-in organizer can open the door without the PIN and hand it over to staff. Sessions last up to 24 hours.
  • Selling: tap products into a cart, take cash (with change due), card, comp, or a payment taken elsewhere with a reference. Free tickets complete without a payment step. Each box office decides whether staff can apply discounts, override prices and ask checkout questions.
  • Card payments run on Stripe Terminal readers (Reader S700, S710 or WisePOS E) paired under the organizer's payout settings. A reader collecting a payment for another live sale is never taken over; the second device can wait, pick another reader or take cash. Readers never ask for a tip.
  • Walk-up buyers don't need an email address. If they give one, tickets are emailed. Sales without one can be renamed later from the dashboard.
  • Scan tab checks existing attendees in, and Orders tab finds, reopens, voids and resends door sales. A sale in progress survives a reload, and one left open on another device can be abandoned from the Orders tab.
  • Seated events: set a ticket count and let best available seat the party together, or tap seats on the door map. Held-back seats need a confirmation before they're sold.
  • Reporting: a sales summary per box office by payment type, operator and day. Door sales are labelled in the orders table and can be filtered by operator, and the orders export gains Sales Channel, Tender, Operator, Amount Tendered, Change Due and Reference columns.

Card readers need the Stripe webhook event terminal.reader.action_failed (see After upgrading).

Team access controls

Give staff access to only what they work on.

  • Roles are now sets of permissions: Admin, Organizer and a new read-only Viewer. Per Organizer or Viewer, admins can turn can see financials and can issue refunds on or off. Admins always have full access.
  • Restrict an Organizer or Viewer to specific organizers or events. An organizer grant covers all of its events, including future ones.
  • Members without financial access don't see revenue, order totals, prices or money in stats and reports.
  • Read-only members don't get the edit and create controls, and the API refuses their changes with 403 READ_ONLY_ROLE.
  • A Team tab on every event and organizer shows who has access, and lets you invite someone straight into that scope. Members can opt in to new-order emails (off by default), and the owner is emailed when someone else adds or invites an admin.
  • The team page was redesigned with search, a role filter, and each member's access, 2FA status and last activity.

Existing members keep whole-account access with their current role. Check-in list links stay shareable door credentials, so a Viewer with the link can still check people in.

Two-factor authentication

  • Turn on authenticator-app 2FA under Settings → Security, with 10 recovery codes and an option to trust a device for 30 days. You're emailed when 2FA is turned on or off, and when a recovery code is used.
  • Account admins can require 2FA for every member of the account from Settings → Team. Members who haven't set it up are asked to before they can do anything else.
  • Admins can reset 2FA for a member who belongs only to their account; superadmins can reset anyone. On a self-hosted install you can also run php artisan user:reset-two-factor {email}.
  • Ten wrong codes lock 2FA sign-in for 15 minutes.

Single sign-on

Social login (core). Sign in, sign up and accept invitations with Google, Microsoft, or one OpenID Connect provider of your choice (Okta, Keycloak, Authentik, Zitadel and others), configured through SSO_* environment variables. The redirect URI to register with each provider is <APP_FRONTEND_URL>/sso/callback. Link or unlink providers under Settings → Security. Local 2FA still applies. Run php artisan sso:status to check your setup; see docker/all-in-one/README.md for a walkthrough.

  • Provider requests are limited to public HTTPS addresses, for this provider and for enterprise connections. Set SSO_ALLOW_PRIVATE_ISSUERS=true only on a single-tenant install with an internal identity provider (an internal Keycloak, for example), where you trust every account admin.
  • SSO_PASSWORD_LOGIN_DISABLED=true turns off password sign-in for everyone except superadmins. Before you set it, make sure people can get in: they must have linked single sign-on from their profile, or the provider must vouch for their email (SSO_OIDC_TRUST_EMAIL=true for your own OIDC provider). If the provider is down, unset the variable and restart to let people back in with their password.

Enterprise connections (Enterprise). Each account can connect its own identity provider (Okta, Microsoft Entra ID, Google Workspace or any OIDC provider) under Settings → General → Single sign-on, with a guided setup and a test sign-in. Included in every Enterprise licence.

  • Verify your email domains with a DNS TXT record. People on a verified domain are sent to your provider from the "Sign in with SSO" button, and can be added to the account on first sign-in. Domains are re-checked daily, so this needs the scheduler, a queue worker and outbound DNS. A domain can belong to only one account; php artisan sso:verify-domain moves a disputed domain on a self-hosted install.
  • Require SSO for the account, with a preview and per-member exemptions. It needs an active connection, a verified domain and 2FA on the owner. Turning it on signs affected members out everywhere and deletes their API keys for that account; they can't reset a password, and nobody can sign up with an email on your domains. Members the connection can't sign in yet, such as people who own another Hi.Events account and haven't connected it, keep their password sign-in, sessions and API keys; the preview and the Team page both show them as Password. Sessions through the connection last 12 hours by default (15 minutes to 7 days).
  • Connect it to your own login. Any active member, the owner included, can connect the account's connection under Settings → Security after confirming their password. The owner can then sign in through the identity provider day to day. Connecting never adds anyone to an account or changes their role. A member who owns another account is covered by Require SSO once they connect it.
  • Platform superadmin logins can't connect to, sign in through, or confirm their identity with an account's connection; they keep password sign-in.
  • If you change the provider's issuer, client ID or endpoints, the client secret must be entered again, so the stored secret is never sent to a new provider.
  • The owner is never required to use SSO and can always sign in with password and 2FA as a break-glass; every admin is emailed when they do.
  • Client secrets are encrypted with APP_KEY. If you rotate it, keep the old key in APP_PREVIOUS_KEYS, or every enterprise sign-in breaks.
  • php artisan sso:unlink-identity removes a person's single sign-on links (for a compromised or replaced identity provider account) and signs them out everywhere.
  • Removing someone from your identity provider doesn't remove them from the team or revoke their API keys. Remove them under Settings → Team as well.

API keys and an in-app API reference

  • Create API keys under Settings → API keys for Zapier, CRMs and your own scripts. A key acts as you, in one account, with your role and access, and never more. Keys can be read-only or read and write, and expire after 7 days to a year (90 days by default), or never. Creating one needs a verified email and confirming your identity. View-only members can only create read-only keys, and each member can have up to 25.
  • Keys can't manage sign-in, the team, payouts, Stripe Connect, card readers, VAT settings, API keys or account settings (403 API_KEY_NOT_ALLOWED). The API reference marks these endpoints "Not available to API keys".
  • A key stops working when you revoke it, an admin revokes it (you're emailed), you leave the account, or your password is reset. Removing a sign-in method from your profile revokes your keys in every account. Turning on Require SSO, or changing, switching off or deleting an enterprise connection, revokes the keys for that account.
  • You're emailed when a key is created, and 7 days before it expires for keys that last 14 days or more.
  • The full key is shown once. Only a hash and the last four characters are stored. Keys share the per-user API rate limit (180 requests a minute by default).
  • Settings → API keys → API reference shows the documented API inside the app, including error codes, rate limits and a backward-compatibility promise for the endpoints keys can call. It works without API_DOCS_ENABLED.

Lineup and schedule

For festivals, conferences and club nights.

  • People: organizer-level profiles for speakers, artists and hosts (photos, tagline, bio, links), reused across events from a new People page.
  • Event lineup and timetable: billing tiers, role labels, hidden acts, stages or rooms, and a drag-and-drop timetable. Late sets stay on their night: schedule days run from 05:00 to 05:00.
  • On the event page: a lineup grid and a schedule timeline with a stage filter, profile and session details, add to calendar, and #session-<id> deep links. The lineup is added to the event's search engine data.
  • Run sheets: export to XLSX, print, or import people and slots from CSV, XLSX or pasted rows. Every row is previewed before anything is saved, and re-importing an export changes nothing.
  • Order and ticket emails can include lineup highlights, also available as lineup.* Liquid tokens.
  • Duplicating an event copies its lineup.
  • Imports take up to 500 people and 1,000 slots at a time.

Customisable homepage sections

Event and organizer pages are now built from sections you can reorder, show, hide and rename: about, lineup, schedule, location and organizer, plus new text, gallery, FAQ, video (YouTube or Vimeo) and sponsor sections. The tickets section (events, on organizer pages) is always shown. Existing pages render exactly as before until you change them.

Saved widgets and new embed types

  • New embeds alongside the ticket widget: a Button that opens tickets in a pop-up, Lineup, Schedule, and an organizer Events list (list, grid or compact, with a category filter and an optional past events tab).
  • Saved widgets: save a widget per event or organizer, and later edits go live without touching your site. Each saved widget shows its views, orders and revenue, and orders record which widget they came from.
  • More options: show only some products or categories, extra event sections, a pinned date for recurring events, a promo code, pop-up or new-tab checkout, and a language. Affiliate, promo code and UTM parameters on the host page are carried into checkout, and the host page receives checkout events for its own analytics.
  • A rebuilt editor with a live preview and install guides for HTML, WordPress, Webflow, Squarespace, Wix, Shopify, Framer and React/Next.js. Sections hidden on the event page show a Hidden badge and can't be turned on in the widget, and sections with no content show an Empty badge, as in the homepage designer.
  • widget.js is now compressed and cached, and loads iframes lazily. Existing embed snippets keep working.

Settings and design

  • One settings area at /settings: Profile and Security for you; General, Organizers, Team, API keys and Taxes and fees for the account. Forms show an unsaved-changes bar. Old /account/* and /manage/profile URLs redirect.
  • Every create and edit modal shares one layout with a sticky footer, and goes full screen on phones.
  • Redesigned event creation, homepage and ticket designers, organizer and event homepages, the products table and the date picker.
  • A purchases view on each product shows who bought it, with search, totals and an export. Orders can be filtered by product.

Security fixes

These affect earlier self-hosted versions.

  • Cross-site request forgery. The login cookie was SameSite=None, so a form on any website could make a signed-in organizer's browser send POST requests to the API. It is now SameSite=Lax whenever the frontend and API are on the same site.
  • Cross-origin reads. With CORS_ALLOWED_ORIGINS=* (the old default), any website could read API responses as a signed-in organizer. See the CORS change below.
  • Stored XSS in the email template preview, where Liquid filters could build markup after purification. Rendered bodies are purified again and the preview is sandboxed.
  • Ids scoped to their parent. Product prices, email templates, waitlist offer prices, order edits and manual-attendee tax ids are now checked against the event or account being edited.
  • Invitations. Revoking an invitation no longer soft-deletes the invitee's whole user, and other accounts can no longer rename a user.
  • Validation cost. Duplicate checks on large lists grew quadratically, so a single request could tie up the server. They are now linear.
  • Server rendering mixed up visitors. Pages rendered at the same moment could be fetched with another visitor's sign-in or language, so, for example, an organizer's unpublished events could appear on a public organizer page served to someone else. Each request now has its own context. Affects server-rendered deployments up to rc.2.
  • SSR logs no longer print request headers, which included login tokens.
  • Clickjacking. App pages (sign-in, dashboard, settings, admin) and the homepage designer previews can no longer be framed by other sites, whatever the letter case or URL encoding of the path. Event, widget and checkout pages are unchanged.

Spam and abuse protection

  • Sign-up is limited per IP address and per email (APP_REGISTER_RATE_LIMIT_PER_HOUR, default 10), with optional Cloudflare Turnstile (TURNSTILE_SECRET_KEY and VITE_TURNSTILE_SITE_KEY).
  • Login is limited to 10 attempts a minute per email, and password resets to 5 an hour per email.
  • Per-IP limits on order creation (APP_PUBLIC_ORDER_RATE_LIMIT_PER_MINUTE, default 60), promo code lookups (APP_PUBLIC_PROMO_CODE_RATE_LIMIT_PER_MINUTE, default 10) and payment intents (APP_PUBLIC_PAYMENT_INTENT_RATE_LIMIT_PER_HOUR, default 120).
  • A payment is cancelled after 5 failed card attempts, and the buyer is asked to start a new order.
  • Person names reject links, email addresses, markup and long digit runs. Names already stored stay editable.
  • Emails for a draft or under-review event carry no event content unless every recipient is a verified member of the account.

For instance operators

  • Superadmin dashboard rebuilt: platform commission, activity, top events and accounts split per currency, upcoming events by size, recent signups and a "needs attention" row. Account pages can impersonate users and schedule or immediately run an account deletion; flagged events can be deleted along with their account.
  • Email delivery tracking. Every sent email is recorded (type and provider message id only, no recipient details). With Postmark you can receive delivery, bounce, complaint, open and click events at /public/webhooks/email-delivery/postmark (POSTMARK_WEBHOOK_SECRET), shown on a new admin page and as a status on each message recipient.
  • Lifecycle emails that nudge stuck organizers are SaaS mode only and do nothing on self-hosted installs.

Everything else

  • Grouped ticket emails. Each email address now gets one email listing all of its tickets. Previously, when several tickets in an order shared an address, only the first was emailed. Custom Liquid ticket templates still send one email per attendee.
  • Refunds on manually created orders. Paid manual orders can now be refunded (recorded as offline refunds), and cancelling one no longer leaves its amount in gross sales.
  • Double payments for the same order, for example from two browser tabs, are refunded automatically.
  • Invoice PDFs render Chinese and Japanese characters, and are much smaller. Korean, Thai and Hindi are not covered yet.
  • The webhook logs modal shows the latest deliveries instead of an arbitrary ten (thanks @zigman79).
  • On phones, form fields no longer zoom the page when tapped, and quantity +/- buttons are always visible.
  • Report date ranges, recurrence weekday and month names, status filters and social link names now follow the selected language. Sign-in pages no longer throw hydration errors in production builds.
  • Stripe refunds. A second partial refund of the same amount within 24 hours (for example, refunding the second of two equal tickets) now goes through instead of sticking at Refund pending, and retrying a failed refund creates a new one. A refund Stripe is still processing stays Refund pending instead of showing as Refund failed, and a second refund can't start until it settles.
  • Marked as paid emails attach the invoice marked Paid, not the earlier Unpaid copy.
  • Change seat on a reserved-seating order page only appears when the change can be made: attendee self-service is on, the guest hasn't checked in, and the date hasn't started or been cancelled.
  • The organizer setting Enable attendee self-service by default now saves.
  • Composing from the Messages page asks you to choose recipients instead of failing, and Message Attendees from a ticket's menu opens with that ticket selected.
  • APP_CDN_URL now works with cached config (php artisan optimize); before, image URLs fell back to the storage disk URL.
  • All-in-one image: generating recurring dates, exporting question answers and recording failed jobs no longer fail with a 500 when DB_CONNECTION isn't set (the job tables defaulted to MySQL). On older images, add - DB_CONNECTION=pgsql to the service environment in docker-compose.yml as a workaround.
  • API: creating or editing a question without product_ids, or a tax or fee without a description, no longer returns a 500, and an unknown question belongs_to value returns a 422 instead of a 500.
  • Translations that showed raw placeholders such as {minutos} or {secondes} were fixed in Spanish, French, Portuguese, Vietnamese, Chinese and Slovak, along with a few mistranslations ("Conectar faixa" for Connect Stripe in Brazilian Portuguese).
  • Server-rendered pages now set <html lang> to the visitor's language, and dates and prices are formatted in that language on the server too, so non-English pages no longer re-render on load.
  • Resetting a password now signs that user out of every session and revokes their API keys.
  • Frontend builds are faster: a patched pofile and SWC instead of Babel.
  • The Playwright suite now has 141 spec files, up from 73 in rc.2, and CI runs it in full against both a self-hosted and a SaaS-mode stack.

Breaking changes

CORS defaults to your frontend

An unset or * CORS_ALLOWED_ORIGINS now means "only the site in APP_FRONTEND_URL", instead of every site. The default also allows the www. or bare-domain counterpart of that URL, and https:// when APP_FRONTEND_URL is http://. If APP_FRONTEND_URL is empty, the old allow-everything behaviour is kept.

The all-in-one image serves the frontend and API from one origin and is unaffected. If your frontend is reachable on other origins (another subdomain or port), list them all in CORS_ALLOWED_ORIGINS, comma-separated. The embeddable widget is an iframe and is unaffected.

Login cookie

The token cookie is SameSite=Lax when the frontend and API are on the same site (the all-in-one image, example.com with api.example.com, localhost ports). Installs that serve them from different registrable domains keep a SameSite=None cookie so login keeps working, but don't get the CSRF protection; move them under one domain to get it.

API changes

  • Offline payment instructions need Manage payments. Changing an event's offline_payment_instructions through PUT or PATCH /events/{event_id}/settings now needs the Manage payments permission (Admins by default) and a signed-in session. API keys, including an Admin's, and Organizer-role members get a 422 on that field when the request would change the saved instructions, including clearing them (a PUT that leaves the field out clears it). Re-sending the saved instructions unchanged, or leaving the field out of a PATCH, works as before, so integrations that read the settings and write them back are unaffected.
  • Deprecated: POST /events/{event_id}/attendees/{attendee_public_id}/check_in. It never recorded the check-in on a check-in list or sent checkin.* webhooks; use the event's check-in lists instead. Its behaviour is unchanged, except that check_out for an attendee who was never checked in returns 409 instead of a 500.
  • Removed POST /events/{event_id}/orders/{order_id}/message. It could not work (it passed an order id where the message job expected a message). Use POST /events/{event_id}/messages.
  • Attendee email can be null in API responses and webhook payloads, for box office sales where the buyer gave no email.
  • Name validation is stricter on checkout, attendee edits, waitlist joins, sign-up and invitations: names containing links, email addresses, < > @ { } or phone-number-like digit runs return 422.
  • Password login with 2FA. When the user has 2FA on, POST /auth/login returns token: null and a challenge to complete at POST /auth/login/two-factor. Scripts that sign in with a password should switch to an API key.
  • Session tokens only in the Authorization header or the login cookie. A JWT passed as ?token= in the URL or as a token body field is no longer accepted. Integrations should use an API key (Authorization: Bearer hie_…).
  • Session tokens can be revoked. A password reset, changes to a user's single sign-on links, and Require SSO revoke that user's existing sessions (401 SESSION_REVOKED), and a deactivated team member is refused at once. Tokens issued before the upgrade stay valid.
  • Money fields are omitted, not null, for members and API keys without can see financials: order and order item totals and prices, event and date sales stats, affiliate total_sales_gross and box office gross_sales. Order short_id also needs edit access. Money filters and sorts are ignored. Reports, invoices, product purchases, VAT settings, the fee preview, webhook management and the order, affiliate and product-purchase exports return 403 PERMISSION_REQUIRED. Existing members keep their access, so nothing changes until you restrict someone.
  • Other users' private fields. pending_email, marketing_opted_in_at, has_password and has_pending_email_change are only returned for your own user.
  • Child ids are checked against their parent. An id that belongs to another event or account (a price, a product, an email template, a waitlist offer price, a tax) now returns 404 or 422 instead of being accepted or silently ignored. /users/{id} and /accounts/{id} only accept numeric ids.
  • Public check-in attendee detail (GET /public/check-in-lists/{id}/attendees/{attendee}/detail) no longer returns order.total_gross or order.currency.
  • New error_code values: SESSION_REVOKED, SSO_REQUIRED, SSO_SESSION_EXPIRED, TWO_FACTOR_CHALLENGE_EXPIRED and API_KEY_READ_ONLY (401); API_KEY_NOT_ALLOWED, READ_ONLY_ROLE, PERMISSION_REQUIRED, ACCESS_RESTRICTED, TWO_FACTOR_SETUP_REQUIRED, ACCOUNT_PENDING_DELETION, IMPERSONATION_NOT_ALLOWED and FEATURE_UNAVAILABLE (403); INVITATION_SIGN_IN_REQUIRED (409); and the box office codes SESSION_EXPIRED, UNAVAILABLE, SALE_EXPIRED and READER_UNAVAILABLE. The in-app API reference lists them all with their statuses.
  • Additive fields: seat_uid and seat_label on attendees, band_key on order items, box_office_* fields on orders, has_seat_map on events, price_before_discount_including_taxes_and_fees on public product prices, schedule_days on public lineup entries, and is_checked_in on attendees in the public order payload. Webhook events are unchanged, and webhook payloads always carry full prices whoever triggered them.

Behaviour changes

  • End dates default to the end of the day. Picking only a date in an end or expiry field (sale end, promo code expiry, check-in list and box office expiry, event or date end) now sets 11:59 PM instead of midnight, so the period runs through the chosen day. Start fields still default to midnight; saved values don't change.
  • Stripe refund emails wait for Stripe. When "Send refund notification email" is ticked, or a cancellation also refunds, the buyer's email is sent once Stripe confirms the refund succeeded, not when it is requested. If the refund fails, no email is sent. Offline and manual refunds still email straight away. On self-hosted installs without Stripe refund webhooks (refund.created, refund.updated) the email is no longer sent; those installs already leave Stripe refunds at Refund pending.
  • Webhooks are retried. A failed delivery (non-2xx, connection error or a timeout over 3 seconds) is retried twice more, after about 10 and 100 seconds, and each attempt shows in the webhook's logs. Delivery is now at-least-once and its order isn't guaranteed, so receivers should de-duplicate on event_type, the payload id and event_sent_at, which stay the same across retries. Very large payloads (over about 120 KB, for example an order with hundreds of attendees) are still sent once without retries.
  • Test messages only go to you. "Send as test" now emails only the sender for every recipient type, sends immediately even if "Schedule for later" is selected, and stays a test if it is held for review. Before, a test to a single buyer or to "Order owners with a specific product" also reached the real buyers. is_test on POST /events/{event_id}/messages now applies to every message_type.
  • Reactivating an attendee checks availability. Activating a cancelled attendee now checks the ticket's quantity (per date where set), its shared capacity and the date's capacity, and returns a 422 when there is no room instead of overselling.
  • Draft event previews in an embedded widget. A signed-in organizer no longer sees a draft event through a widget embedded on another site, because the login cookie isn't sent there. Preview drafts in the app.
  • Ordering on unpublished events is limited to members of the account that owns the event, and is refused while the event is under review. Waitlist joins and contacting the organizer need a live event.
  • Email changes are confirmed at the new address, and the old address gets a notice. Changing your email needs your password or a fresh single sign-on, and members of an account that requires SSO can't move their email off its verified domains.
  • Team invitations need a verified account. Someone who already has a Hi.Events login now signs in and joins (409 INVITATION_SIGN_IN_REQUIRED) instead of setting a new password from the invitation. Invitations to an account that requires SSO go through single sign-on. Account and sign-in emails no longer greet people by name.
  • Settings moved to /settings/*. Old /account/* and /manage/profile links redirect.
  • New-order emails to team members are opt-in per member, separate from the event's organizer notification setting.
  • First day of the week defaults to Monday for every organizer. Organizers whose calendars should start on Sunday can change it in the organizer settings.
  • Prices follow the app language. Prices are formatted in the visitor's selected language rather than the browser's locale.
  • Event messages are marked Sent once the email has actually been sent, and Failed only after every retry.
  • Exports. The orders export gains six columns at the end and the attendees export gains Sales Channel at the end. For seated events the attendees export also gains a Seat column before the question columns.
  • Late payments. A payment that lands on an expired or cancelled order is still refunded and the buyer is still emailed, but it no longer sets the order's refund status, changes event statistics or sends the order.refunded webhook, because the sale was never counted.
  • Embedded widgets. Old snippets that hardcode data-hievents-locale="en" with widget-version="1.0" now follow the visitor's language. Widget iframes load lazily and are display: block.
  • Every event has a box office row, created by migration for existing events. It does nothing until a PIN is set or an organizer opens it, and needs a licence to be used.
  • Public compliance endpoint. GET /compliance on the frontend (and GET /public/compliance on the API) returns APP_PLATFORM_SUPPORT_EMAIL, the licence status and whether the attribution terms are met. Set APP_PLATFORM_SUPPORT_EMAIL to a real address.
  • Google Tag Manager. On self-hosted installs, only account admins can add a new container, and API keys never can.
  • Outbound links from organizer and event content are nofollow ugc, and pages carry noindex when indexing is turned off. Widget pages (/widget/*) send X-Robots-Tag: noindex, so search engines don't list them separately from the page they're embedded on.

Enterprise features and licensing

Reserved seating, the box office and enterprise single sign-on connections are Enterprise features. Their code lives in backend/ee/ and frontend/src/ee/, ships in this repository, and is covered by ee/LICENCE rather than the AGPL. Using them for real events needs a licence key from https://hi.events/licensing, set in APP_LICENCE_KEY. A key unlocks the Enterprise features its plan includes, and also removes the "Powered by Hi.Events" notice. Development and testing are free: APP_LICENCE_KEY=development unlocks all three on a test install. Without a key they stay locked and nothing else changes.

Everything else in this release is core and free to self-host, including social login, two-factor authentication, team access controls and API keys. Without a licence, the "Powered by Hi.Events" attribution requirement in the root LICENCE is unchanged.

How licences work

  • Licence status, expiry and the features in use are shown in a Licence dialog in the avatar menu, and on a new superadmin Licence page.
  • A licence includes the features of its plan. Setting up a feature your licence doesn't include is refused, and the Licence dialog names what isn't included and how to add it.
  • Licences that cover a yearly sales volume show it on the superadmin Licence page, next to the instance's net sales by currency for the current and previous licence year.
  • A licence has a 30-day grace period after it expires. Once it lapses, setting up anything new for an Enterprise feature is refused with 403 FEATURE_UNAVAILABLE, and the setup pages show a "Setup is locked" notice. What is already set up can still be viewed and removed, and online ticket sales for events with reserved seating keep working. New box office sign-ins are refused, so open box office sessions end within 24 hours.
  • Enterprise SSO connections keep signing people in after a lapse, including where SSO is required, so you can wind them down. They can be switched off or deleted, but not changed or switched back on until the licence is renewed.
  • The Licence dialog lists, for each Enterprise feature you use, what keeps working and what stops. The licence text in backend/ee/LICENCE and frontend/src/ee/LICENCE says the same and names enterprise single sign-on as a licensed feature.
  • An invalid key is logged once a day rather than on every request.

What's Changed

✨ Features

🐛 Fixes

🔧 Improvements

🧰 Development

  • Amp orb setup, resume and services by @daveearley in #1387
  • Run the full E2E suite against self-hosted and SaaS, fix flaky specs by @daveearley in #1396
  • Move agent instructions to AGENTS.md with path-scoped rules by @max-1-max in #1409
  • Update the README for 2.0 and translate it into every supported language by @daveearley in #1422

Commits without a pull request

  • Allow refunds on manually created orders (303f4cc0c)
  • Mark Stripe refunds as pending before the refund is created (2c18a3dfc)
  • Move enterprise SSO into ee behind a licence (b30bc05cd)
  • Flag widget sections the event page hides (967763639)
  • Default the box office feature flag to off (8c582a232)
  • Let logged-out users open the lifecycle email unsubscribe page (03ff193e7)
  • Show image insert errors on the upload tab (ef28ae10d)

New Contributors

Full Changelog: v2.0.0-rc.2...v2.0.0-rc.3


Open questions for Dave

Not release-note text. Remove this section before publishing.

  1. Login cookie wording. Your working draft says installs on different registrable domains "will lose cookie login". AuthCookieSameSite::forRequest (#1372) still sends SameSite=None for cross-site requests, and AuthCookieSameSiteTest pins that, so the notes say those installs keep working but don't get the CSRF fix.
  2. ee/LICENCE final review. The lapse sentences were reworded to match the code (door sign-ins stop, online sales and SSO sign-in continue); the rest still needs your legal review, and the README doesn't mention ee/.
  3. IdP offboarding. The line about removing people from your identity provider (now under Enterprise connections) describes current behaviour. Keep it.
  4. Main-only PRs. #1389 and #1400 were merged straight into main and repeat work from develop PRs. The list uses the develop PRs; GitHub's generated list will include both if the tag is cut from main.
  5. @max-1-max is listed as a new contributor. Keep that, or credit those PRs differently?
  6. Public /compliance endpoint publishes APP_PLATFORM_SUPPORT_EMAIL, the licence status and whether branding was removed without a licence. Keep it in public notes?
  7. APP_LICENCE_KEY=development is mentioned because the Licence dialog does. Fine to advertise?
  8. Cloud-only items (lifecycle emails, Postmark delivery tracking) are kept to a few lines. Cut if you prefer.
  9. Licence plans. #1425 split the plan into own_events (white label only) and platform (white label, seating, box office, enterprise SSO), with an optional covered sales volume. The notes describe plans generically. Name them here, or leave that to /pricing while it's reworked? Keys issued with the old enterprise plan are now rejected ("The licence key is for an unknown plan"), so reissue any you've handed out before tagging. Cloud keys (plan cloud) are unaffected.
  10. Publish rc.3 as a pre-release. The Docker Hub step says release candidates aren't tagged latest, which only holds for pre-releases (post-release-push-images.yml).
  11. rc.1 notes give a broken all-in-one command. docker compose pull && docker compose up -d doesn't upgrade a repo clone, because the compose file builds from source. Edit the rc.1 notes to point v1 users here?
  12. v1.11.1 → rc.3 hasn't been rehearsed. The July staging rehearsal predates every rc.2 and rc.3 migration.
  13. All-in-one compose gaps. It doesn't pass APP_PLATFORM_SUPPORT_EMAIL, API_DOCS_ENABLED, OCCURRENCES_QUEUE_NAME, POSTMARK_WEBHOOK_SECRET, AWS_SESSION_TOKEN or the spam check settings. Worth adding at least the support email before tagging, since the notes ask everyone to set it.

Don't miss a new Hi.Events release

NewReleases is sending notifications on new releases.