github HiEventsDev/Hi.Events v2.0.0

latest release: v1.11.3-beta
4 hours ago

Hi.Events v2.0.0

This is a big one! A massive thanks you to all contributors and testers.

Important

Upgrading a self-hosted install from v1.x? Back up your database first: v2 changes how event dates are stored and several migrations can't be undone. Read Upgrading from v1.x below before you start. Installs set up without Docker must also check their JWT_SECRET (step 6).

The first stable release of v2, and the biggest release since the project started. v2 adds recurring and multi-date events, embedded checkout, a lineup and schedule, team permissions, two-factor authentication, single sign-on, API keys and redesigned event pages, and has been through a full security review.

Everything in this release is free to self-host under the AGPL, except reserved seating, the box office and enterprise SSO connections, which need a licence. See Enterprise features and licensing at the end. If you’ve bought a licence, email hello@hi.events and we’ll help you upgrade.

What's new

Events and dates

  • Recurring and multi-date events: daily, weekly, monthly and yearly schedules with several times a day, up to 1,200 dates per event. Each date can have its own capacity, prices, products, venue and ticket quantity.
  • Date management: cancel one date and refund its orders in one go, bulk edit times, capacity and venues across dates, and filter attendees, orders, reports and exports by date.
  • Switch event type: turn a single event into a recurring one, or a recurring event with one date and no orders back into a single event.
  • Reusable locations per organizer, with optional Google Places address autocomplete, online events, and a different venue per date.
  • Sequential tier release: the next price tier goes on sale when the one before it sells out.

Selling

  • Embedded checkout: the whole checkout, payment included, runs in a pop-up over your own site, with Apple Pay and Google Pay.
  • Product add-ons: attach merch, parking or upgrades to a ticket, or sell products only as add-ons.
  • Payments per organizer: Stripe Connect, VAT and fees move from the account to each organizer, so one account can pay out to several Stripe accounts.
  • Product purchases view: see who bought each product, with search, totals and an export, and filter orders by product.
  • Refunds on offline and manual orders, in full or in part.
  • Grouped ticket emails: each email address gets one email with all of its tickets.
  • Redesigned checkout and ticket selection, with a step counter, a reservation timer and a cart button that follows you down the page.

Event pages and embeds

  • Redesigned event and organizer pages with four layouts: Marquee, Flyer, Story and Invite. Pages load faster, don't shift as they load, and score higher for accessibility. See Faster, more accessible pages below.
  • 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, sponsor and "More from this organizer" 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.

Check-in

  • Rebuilt check-in app with Scan, Search and Stats tabs, inline camera scanning, USB scanner mode, live throughput, undo and an offline indicator.
  • Control what a public check-in link shows: attendee notes, question answers and order details are each off by default.

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

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. Optional interactive docs at /docs/api.
  • Webhook retries and an occurrence.cancelled webhook.

Around the app

  • One settings area at /settings for your profile, security, team, API keys and account, with an unsaved-changes bar.
  • Redesigned dashboards with sparklines, period comparisons, a setup checklist and a publish check that catches missing tickets, dates or payment setup.
  • Redesigned event creation, homepage and ticket designers, products table, product editor (with a live widget preview), date picker and duplicate event dialog. Modals go full screen on phones.
  • Account deletion with a 30-day grace period.
  • Nine new event categories, each with its own wording ("Classes" and "Book now" for a wellness event).
  • First day of the week setting for organizers, used by every calendar (thanks @cornelisonc).
  • Japanese, Korean and Finnish translations (thanks @Tt0x for Japanese), and improved Polish.

Security and self-hosting

  • Security review: v2 fixes cross-site request forgery, cross-origin API reads, stored XSS and a server-rendering mix-up that affected earlier self-hosted versions, and every known advisory in its dependencies. See Security below.
  • Spam and abuse protection: rate limits on sign-up, login, orders and promo codes, optional Cloudflare Turnstile, stricter checks on names, and optional AI checks on newly published events.
  • Cookie consent banner you can turn on for your install.
  • Announcements: post in-app banners or pop-ups to users on your instance.
  • 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.

Licensed features

Reserved seating, a box office and enterprise single sign-on connections are also new, for installs with a licence. See Enterprise features and licensing at the end.


Upgrading from v1.x

Hi.Events Cloud is upgraded for you. Go straight from v1.x to v2.0.0: you don't need to install any release candidate first.

The quick and risky upgrade

For test, staging and hobby installs on the all-in-one Docker setup, where losing a few minutes of orders wouldn't matter. It skips taking the site offline, so orders placed while it runs may be missing from the backup. Run this in docker/all-in-one:

docker compose exec -T postgres pg_dump -U postgres -F c hi-events > hievents-pre-v2.dump
git fetch --tags && git checkout v2.0.0
docker compose up -d --build

The first command takes a backup. Migrations run when the container starts. Then go through steps 6 and 7. If something goes wrong, restore the backup and follow the full steps below.

Upgrading with an AI agent

If you use a coding agent such as Claude Code, Codex or Cursor on the server, you can give it this prompt. It follows the full steps below and stops to ask before anything it can't undo.

Upgrade this self-hosted Hi.Events install from v1.x to v2.0.0.

Read the release notes first: https://github.com/HiEventsDev/Hi.Events/releases/tag/v2.0.0
Follow "The full upgrade" steps in order. Do not use the quick upgrade.

1. Work out how this install runs: the all-in-one Docker setup from a clone of the repo,
   Docker Hub images, separate Docker services, or a manual install. Tell me what you
   found and the version it is on now, then wait for me to confirm.
2. Check the requirements in step 1 and report anything missing.
3. Tell me before you take the site offline (step 2), and wait for me to say go.
4. Empty and stop the queue workers if step 3 applies.
5. Take the backup in step 4. Confirm the dump file exists, is not empty, and lists
   tables with `pg_restore -l`. Tell me where it is. Do not continue without a good backup.
6. Upgrade and migrate as step 5 describes for this setup. If a migration fails, stop
   and show me the error. Do not retry, edit migrations or change the database by hand.
7. Do step 6: check whether JWT_SECRET or APP_KEY still hold the published example
   values. If JWT_SECRET does, replace it the way step 6 says for this setup, and
   warn me first that this signs everyone out. Ask me before changing APP_KEY.
8. Go through the checks in step 7 and report each one: the app loads, an event with
   past sales shows its date and sales, the scheduler and queue worker are running,
   and any CORS, proxy or docker-compose.yml changes this install needs.
9. Finish with a short summary: what you changed, where the backup is, and anything
   I still need to do.

Never drop, restore or edit the database, delete the backup, or remove Docker volumes
unless I ask you to in this conversation. If something goes wrong, stop and tell me,
and show me the "Rolling back" steps from the release notes instead of running them.

The full upgrade

For production installs, and every install that isn't the all-in-one Docker setup. Follow these steps in order.

Taking the site offline before the backup is what makes the backup a real rollback point: restore it and you're back exactly where you were, with nothing lost.

1. Check the requirements

v1.11 v2.0
PHP 8.2 8.3 or higher
PHP extensions intl intl, gd
Laravel 12 13
Node (to build the frontend) not pinned 22.22 or higher

The Docker images already include all of this. On a manual install:

php -v && php -m | grep -E "^(intl|gd)$" && node -v

2. Take the site offline

Stop the app but leave the database running. You need it for the backup and the migration.

docker compose stop all-in-one

With separate services, stop the web-facing ones and leave postgres up. On a manual install, stop your app server or hold traffic at your reverse proxy.

Migrations take seconds on most installs and a few minutes with hundreds of thousands of attendees. Taking the site offline also removes the one way they can fail: two migrations add unique indexes after removing duplicate rows, and a write in between makes them fail.

3. Let the queue empty, then stop the workers

Skip this with QUEUE_CONNECTION=sync or the all-in-one image (stopping the container stopped its worker).

Let any separate queue worker finish its backlog, so the backup holds every order. php artisan queue:monitor default,webhook-queue shows how many jobs are left (add your OCCURRENCES_QUEUE_NAME queue if you set one). When every queue is at 0, stop the worker the way you run it, for example docker compose stop <worker service> or supervisorctl stop <program>. Workers keep running the old code until they're restarted, so start them again after the upgrade.

4. Back up the database

v2 rewrites how event dates are stored and removes duplicate rows. Several migrations can't restore their data on rollback, so this backup is your only real way back.

docker compose exec -T postgres pg_dump -U postgres -F c hi-events > hievents-pre-v2.dump

If postgres runs on the host: pg_dump -U your_user -d your_database -F c -f hievents-pre-v2.dump.

5. Upgrade and migrate

  • Docker from a clone of this repo (the README install): git fetch --tags && git checkout v2.0.0, then docker compose up -d --build in docker/all-in-one. Migrations run before the app starts. docker compose pull alone doesn't upgrade a clone, because this compose file builds from source.
  • Images from Docker Hub: set your image tags to v2.0.0 (or latest), then 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: npm ignores the resolutions block), then start your app server and queue workers again.

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

6. Check your secrets

v1's backend/.env.example shipped a sample JWT_SECRET and APP_KEY, and installs set up without Docker often kept them. While JWT_SECRET still holds the published value, the v2 API answers every request with a 503, so the dashboard and event pages don't load, and the log says why. Replace it (this signs everyone out):

  • Manual install: run php artisan jwt:secret --force in backend/, then restart your app server and queue workers.
  • Docker: put a new value on the JWT_SECRET line of your .env (openssl rand -base64 32 makes one), then run docker compose up -d. Running jwt:secret inside the container doesn't help, because the container's environment variable takes priority.

If APP_KEY is still base64:DwMidgIu8YVSEg0BLMrh5JS2dk1POpCn3rvDaZk3fEQ=, replace it too, and keep the old value in APP_PREVIOUS_KEYS so encrypted data stays readable. Docker installs that generated their own secrets, as the README asks, are unaffected.

7. After upgrading

What changes for v1 installs:

  • Each existing event becomes a single-date event. Buyers see no difference, and the date picker only appears on recurring events.
  • Event and organizer pages get the new design. Events use the Story layout until you pick another in the homepage designer.
  • Addresses move into each organizer's location library.
  • Stripe Connect and VAT settings move from the account to each organizer. Existing connections carry over, so nobody has to reconnect. Payout settings are now under organizer settings.
  • 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.
  • Account settings moved to /settings. Old links redirect.
  • API integrations: some endpoints and fields changed. See Breaking changes below.

Check these:

  • Open an event with past sales. It should show its original date, and its dashboard its sales. If sales show as zero but the orders are there, open an issue with the event id.
  • 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 recurring date generation, account deletion, waitlist offer expiry, scheduled messages, 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.

Upgrading from a release candidate

Follow steps 2 to 7. Requirements are the same as rc.3, except that building the frontend from source now needs Node 22.22 or higher. Step 6 applies to you too, and Since rc.3 further down lists the rest.

New environment variables

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

Variable Default What it does
GOOGLE_MAPS_API_KEY unset Address autocomplete. Without it you type addresses by hand.
SSO_* unset Social login with Google, Microsoft or an OpenID Connect provider. See Single sign-on below.
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_PUBLIC_ORDER_RESERVED_QUANTITY_PER_IP unset (off) Caps how many tickets one IP address can hold in live reservations on an event.
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.
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.
APP_LICENCE_KEY unset Unlocks Enterprise features. development unlocks them for testing.
APP_SEAT_MAP_MAX_SEATS 3000 Largest seat map that can be saved.

Rolling back

Treat v2 as forward-only, and restore your backup if you need to go back. Because it was taken with the site offline, it puts you back exactly where you were. Decide before the upgraded site takes new orders, since a restore loses anything written after the backup.

With the all-in-one Docker setup, run this in docker/all-in-one, replacing v1.11.1-beta with the version you upgraded from:

docker compose stop all-in-one
docker compose exec -T postgres dropdb -U postgres hi-events
docker compose exec -T postgres createdb -U postgres hi-events
docker compose exec -T postgres pg_restore -U postgres -d hi-events < hievents-pre-v2.dump
git checkout v1.11.1-beta
docker compose up -d --build

On other setups, stop the app, drop and recreate the database, restore the dump with pg_restore, and start your previous version again. Restore into an empty database: restoring over the upgraded one leaves v2's tables behind.

The migrations define down() methods, but several can't restore what they changed: the two de-duplicating migrations only drop their index, events in the nine new categories become OTHER, and events created under v2 have no date in v1.

Troubleshooting

  • The container keeps restarting. A migration failed. Read docker compose logs.
  • Every request returns 503. JWT_SECRET still holds the published example value. See step 6 of the upgrade.
  • Creating a recurring schedule never finishes. Nothing is consuming the queue the date jobs go to. Check your worker, and OCCURRENCES_QUEUE_NAME if you set it.
  • A unique index migration failed. Something wrote a conflicting row during the migration. Stop the app and run php artisan migrate --force again.
  • composer install fails on the PHP version. v2 needs PHP 8.3 or higher.
  • The frontend build fails on Node. v2 needs Node 22.22 or higher.
  • Address search shows no suggestions. Expected without GOOGLE_MAPS_API_KEY. Use "Enter address manually".

In detail

Recurring and multi-date events

Every event now has one or more dates.

  • Schedules: daily, weekly, monthly and yearly, with intervals, several times a day, and a label and duration per time slot. Monthly supports "the 15th" and "the last Friday". End after a number of dates or on a day. Up to 1,200 dates per event.
  • Per date: capacity, price overrides, which products are on sale, venue, and ticket quantity (a ticket's quantity can apply to each date or across all of them).
  • Cancel a date and refund its orders in one action, and notify its attendees. Orders that span several dates are left for a manual refund.
  • Bulk edit times, duration, capacity, labels or venue across dates, with options to change only future dates and skip dates you edited by hand.
  • Reporting: attendees, orders, waitlist entries, reports and exports filter by date, and an Occurrence Summary report shows sales, attendance and check-ins per date.
  • Buyers get a calendar and time-slot picker on the event page and in the widget, with remaining-capacity hints, sold-out states and ?occurrence_id= deep links.
  • Waitlists are per date. One person can wait for several dates, offers name the date, and cancelling a date releases its waitlist.
  • Event type: Event settings has an Event Type card. A single event can become recurring at any time (permanently, once it has orders). A recurring event can become single when it has exactly one date that isn't cancelled and no orders.

Embedded checkout

The widget now runs the whole checkout in a pop-up over your page, from choosing tickets to the confirmation.

  • It renders in a shadow DOM, so your site's CSS can't break it and it can't leak styles into your page. It goes full screen on phones.
  • Apple Pay and Google Pay work inside it. Payments that redirect for authentication (3D Secure, wallets) come back into the pop-up, and the redirect parameters are removed from your URL.
  • Closing the pop-up mid-checkout asks first and cancels the order. A reload or accidental navigation reopens the checkout where the buyer left off, while the reservation lasts.
  • The host page receives checkout events for its own analytics, including hievents:order-completed once per order.
  • Set data-hievents-checkout="new-tab" to keep opening checkout in a new tab.

The embed snippet hasn't changed. Existing embeds get all of this without any edits to your site.

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

Event and organizer pages

  • Four layouts for event pages, picked in the homepage designer: Marquee (wide cover, tickets alongside as you scroll), Flyer (your artwork as a poster beside the details), Story (one column, the default) and Invite (a compact, centred card for smaller events).
  • Customisable sections: reorder, show, hide and rename about, lineup, schedule, location and organizer, and add text, gallery, FAQ, video (YouTube or Vimeo), sponsor and More from this organizer sections. The tickets section (events, on organizer pages) is always shown.
  • A section bar on large screens, and a price and checkout bar on phones.
  • Organizer pages show a "Next up" event and a month-by-month list of events.
  • The contact form follows the event or organizer theme.

Faster, more accessible pages

The public pages were rebuilt with Lighthouse scores in mind. Event pages now score up to 99 for performance on desktop (83 on mobile), 96 for accessibility and 100 for SEO, with almost no layout shift.

  • No layout shift or white flash on load. Production server rendering now links each page's styles in the <head>, so the page paints styled at once instead of shifting when the app starts.
  • Images sized for the screen. With an ImageKit CDN in APP_CDN_URL, pages request images at the width they're shown. Covers are never cropped.
  • Less JavaScript up front. Date-formatting locales and the rich text editor load only when a page needs them, and theme colours are worked out on the server.
  • No re-render on load for visitors in other languages: <html lang>, dates and prices are rendered in the visitor's language on the server.
  • Contrast and accessibility. Accent-coloured text is darkened until it meets WCAG AA contrast, new events default to an AA-compliant accent (#7c3aed), focus states are visible, animation respects reduced motion, and the ticket panel is a labelled region.
  • Search engines get structured data for event and organizer pages and the lineup, nofollow ugc on links in organizer content, and noindex on widget pages and when indexing is turned off.

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

Check-in

  • The scanner has three tabs: Scan, Search and Stats. Camera scanning is inline, and USB scanner mode shows that it's listening and what it has read.
  • Stats show totals, progress and live throughput over the last five minutes.
  • An attendee sheet shows check-in history, and accidental check-ins can be undone.
  • Recurring events have a date filter, so door staff only see the session they're working.
  • Haptic feedback and an offline indicator.
  • Check-in lists moved to a table, and each list controls what its public link shows. Attendee notes, question answers and order details are each off by default.

Payments per organizer

  • Stripe Connect, VAT settings and fees live on each organizer, so one account can pay out to several Stripe accounts. Existing connections are migrated, so nobody reconnects.
  • On Hi.Events Cloud (SaaS mode), organizers in one account can share a Stripe connection.
  • Disconnect Stripe from an organizer without touching the Stripe account.
  • Change an event's currency yourself, as long as it has no paid orders.
  • Fixed-amount promo codes can discount the order total instead of each product.
  • Offline payment orders and manually created orders can be refunded in full or in part. This records the refund and can email the buyer; it doesn't move money.

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.

  • 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 the 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. Viewers 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. Set API_DOCS_ENABLED=true for interactive docs at /docs/api, or export the OpenAPI spec with php artisan scramble:export.

Settings, dashboards 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.
  • Dashboards have sparklines and period comparisons (last 7, 30 or 90 days, or the first days of an event's life), a setup checklist that disappears when you're done, and a publish check that warns about missing tickets or dates and blocks publishing until payments are set up. Recurring events get a panel for the next date, with a countdown that becomes a check-in shortcut once it starts.
  • Checkout: a step counter and a reservation timer that turns red as it runs down. One-price tickets show inline, descriptions sit behind a Details toggle, fees and tax have a breakdown, and low stock shows how many are left. The floating "Get tickets" button becomes a checkout button with your cart total, and its label is configurable.
  • Tickets were redesigned, and cancelled or unpaid tickets show as void instead of a scannable code.
  • Products are created and edited in a wide drawer with a live preview of the real widget.
  • Duplicate event groups its options with a line on what each copies, keeps the event's length when you move its start date, and copies email templates. Duplicates also keep sale dates in non-UTC events, question descriptions and order, category order, and image metadata.
  • Every create and edit modal shares one layout with a sticky footer, and goes full screen on phones.
  • Event categories: Family, Hobbies, Wellness, Spirituality, Outdoors, Tours, Film, Dance and Seasonal are new, and the category sets the default wording.
  • Account deletion from Settings → General: a 30-day grace period with a reminder 7 days before, cancellable at any time. Accounts that never completed an order are deleted; the rest are anonymised so financial records stay intact. Live events go to draft straight away, and deletion is blocked while upcoming events have orders.

Security

v2 was reviewed for security before release. These fixes affect v1 and earlier v2 release candidates.

Fixed in v2

  • 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 CORS defaults to your frontend under Breaking changes.
  • 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 checked against the event or account being edited. Public check-in only finds attendees of the list's own event.
  • Invitations. Revoking an invitation no longer soft-deletes the invitee's whole user, and other accounts can no longer rename a user.
  • Emailed links carry a purpose. Invitation, email confirmation and email change tokens only work on their own page, and confirmation links are tied to the address they were sent to. Links already sent keep working.
  • Published example secrets. The sample JWT_SECRET and APP_KEY are gone from .env.example, and v2 refuses to run with the old JWT_SECRET (see step 6 of the upgrade).
  • Validation cost. Duplicate checks on large lists grew quadratically, so a single request could tie up the server. They are now linear.
  • Shared capacity is re-checked under the order lock, so concurrent orders can't overfill a pool.
  • Server rendering mixed up visitors. Pages rendered at the same moment could use another visitor's sign-in or language, so, for example, an organizer's unpublished events could appear on a public page served to someone else. Each request now has its own context.
  • 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. Event, widget and checkout pages are unchanged.
  • Structured data on organizer pages is escaped like the event page's, and embed parameters are only read when the page is actually inside an iframe.

Dependencies. Every open composer audit and yarn audit advisory for production packages is cleared, including critical issues in mtdowling/jmespath.php (code injection) and proxy-addr (IP spoofing, which affected rate limiting), file reads in dompdf (organizer HTML on invoices is rendered with it), and XSS and open redirects in react-router and prosemirror-view. The Docker images are built on current PHP releases.

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), and an optional cap on the tickets one IP address can hold in reservations (APP_PUBLIC_ORDER_RESERVED_QUANTITY_PER_IP). The cap only counts other live reservations, so a single order is never refused on its own.
  • 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.
  • With APP_EVENT_SPAM_CHECK_* and ANTHROPIC_API_KEY set, newly published events are checked with Claude, and suspicious ones are held for manual review.

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.
  • Announcements: post in-app banners or pop-ups to everyone, specific accounts or specific users, with seen and dismissed counts. They're entirely local to your install; nothing is pushed in from outside.
  • Cookie consent: a banner for your install (VITE_COOKIE_CONSENT_*).
  • 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.
  • The Platform fees report is only listed in SaaS mode, like the Platform Fees settings.

Everything else

  • Grouped ticket emails. Each email address gets one email listing all of its tickets. Before, when several tickets in an order shared an address, only the first was emailed. Custom Liquid ticket templates still send one email per attendee.
  • Double payments for the same order, for example from two browser tabs, are refunded automatically.
  • Stripe refunds. A second partial refund of the same amount within 24 hours 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, and a second refund can't start until it settles.
  • Marked as paid emails attach the invoice marked Paid, not the earlier Unpaid copy.
  • Invoice PDFs render Chinese and Japanese characters and are much smaller. Korean, Thai and Hindi are not covered yet.
  • Invited users stuck at login. Someone whose only membership is a pending invitation (for example after resetting their password instead of opening the invite) is told to open the invitation email.
  • The organizer orders list no longer includes orders from deleted events, so it matches the dashboard.
  • iDEAL and other Stripe payment methods are prefilled with the buyer's name from the details step, and payment forms no longer leave a gap above the pay button.
  • Collapsed tickets keep their quantity stepper, add-ons and limit errors. The waitlist banner and other checkout details follow the checkout theme.
  • 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 follow the selected language.
  • The webhook logs modal shows the latest deliveries instead of an arbitrary ten (thanks @zigman79).
  • 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 works with cached config (php artisan optimize).
  • All-in-one image: generating recurring dates, exporting question answers and recording failed jobs no longer fail when DB_CONNECTION isn't set.
  • 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.
  • Translations that showed raw placeholders such as {minutos} or {secondes} were fixed in Spanish, French, Portuguese, Vietnamese, Chinese and Slovak.
  • Resetting a password signs that user out of every session and revokes their API keys.
  • Frontend builds are faster (a patched pofile and SWC instead of Babel), and the frontend and end-to-end tests type-check cleanly.
  • k6 load test scripts are in misc/k6/.
  • Tests: v2 was built alongside a Playwright end-to-end suite that drives the real app in a browser: 141 spec files and over 860 tests, run in full in CI against both a self-hosted and a SaaS-mode stack.

Breaking changes

These matter if you upgrade from v1.x, or call the API from scripts or integrations.

Requirements

PHP 8.3 or higher with intl and gd, and Node 22.22 or higher to build the frontend. See step 1 of the upgrade.

Example secrets are refused

The API answers every request with a 503 while JWT_SECRET is the value v1 shipped in backend/.env.example. Replace it as described in step 6 of the upgrade.

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

Removed and moved endpoints. Stripe and VAT moved from the account to the organizer:

Removed Use instead
GET /accounts/{id}/stripe/connect_accounts GET /organizers/{id}/stripe/connect_accounts
POST /accounts/{id}/stripe/connect POST /organizers/{id}/stripe/connect
GET /accounts/{id}/vat-settings GET /organizers/{id}/vat-settings
POST /accounts/{id}/vat-settings POST /organizers/{id}/vat-settings
PUT /admin/accounts/{id}/vat-settings PUT /admin/organizers/{id}/vat-settings
PUT /admin/accounts/{id}/configuration PUT /admin/organizers/{id}/configuration
POST /events/{id}/questions/export (ran the orders export by mistake) POST /events/{id}/orders/export or /events/{id}/questions/answers/export
POST /events/{event_id}/orders/{order_id}/message (never worked) POST /events/{event_id}/messages

Events and dates

  • location_details is gone from event, event settings and organizer settings payloads, replaced by event_location. is_online_event and online_event_connection_details moved inside it. Creating or updating an event no longer takes an inline address: create a location with POST /organizers/{id}/locations, then pass event_location.location_id.
  • start_date and end_date on events are derived from the event's dates. For a recurring event they're the first start and last end in the returned window. Events can no longer be sorted or filtered by them, and the default sort changed from start_date asc to created_at desc.
  • The events.start_date and events.end_date columns are no longer read or written. They freeze when you upgrade. Point reports or jobs that query them at event_occurrences.start_date.
  • Checkout takes event_occurrence_id per product line. Single-date events fill it in, so existing integrations keep working. Recurring events require it.
  • Add-on-only products appear in the public products list. Filter on is_addon_only if you render products yourself.
  • An order-level fixed-amount promo code can split one line into two order items with the same product_price_id, so the discount divides exactly. Code that keys order items by product_price_id needs adjusting.
  • event_settings.hide_getting_started_page was removed. Image uploads are capped at 5 MB, down from 8 MB.

Sign-in and access

  • 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.
  • 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.
  • Offline payment instructions need Manage payments. Changing an event's offline_payment_instructions through PUT or PATCH /events/{event_id}/settings needs the Manage payments permission (Admins by default) and a signed-in session. API keys and Organizer-role members get a 422 when the request would change the saved instructions, including clearing them (a PUT that leaves the field out clears it). Re-sending them unchanged, or leaving the field out of a PATCH, works as before.
  • Other users' private fields. pending_email, marketing_opted_in_at, has_password and has_pending_email_change are only returned for your own user.

Validation

  • 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) returns 404 or 422 instead of being accepted or ignored. /users/{id} and /accounts/{id} only accept numeric ids.
  • Names are checked on checkout, attendee edits, waitlist joins, sign-up and invitations: names with links, email addresses, < > @ { } or phone-number-like digit runs return 422.
  • Waitlist joins are stricter. POST /public/events/{event_id}/waitlist refuses hidden prices, products hidden behind a promo code, and prices outside their sale window (409). To join a promo-gated product, send the new optional promo_code.
  • POST /public/ticket-lookup is limited to 10 requests a minute, and creating an order can return 429 when the optional reservation cap is on.

Other changes

  • Attendee email can be null in responses and webhook payloads, for box office sales where the buyer gave no email.
  • Public check-in attendee detail no longer returns order.total_gross or order.currency, and scanning a ticket from another event returns "Invalid attendee code detected" instead of naming the attendee.
  • 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 check-in lists instead. check_out for an attendee who was never checked in returns 409 instead of 500.
  • New error_code values: SESSION_REVOKED, SSO_REQUIRED, SSO_SESSION_EXPIRED, TWO_FACTOR_CHALLENGE_EXPIRED, API_KEY_READ_ONLY and INVITATION_PENDING (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.
  • New endpoints and fields: PUT /events/{event_id}/type, duplicate_email_templates on event duplication, promo_code on waitlist joins, 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.

Webhooks

  • New event: occurrence.cancelled, when a date is cancelled. Existing events are unchanged, and payloads always carry full prices whoever triggered them.
  • Deliveries 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. Payloads over about 120 KB are sent once without retries.

Behaviour changes

  • A "Default check-in" list is created for every event and can't be deleted. Its public link (with an unguessable id) has no activation or expiry window, so it's live once you upgrade. It shows only attendee names and check-in status unless you turn on the new visibility settings.
  • Check-in lists tied to one date become all-dates lists, and duplicate check-in records are collapsed (soft-deleted, never removed), so check-in counts may drop slightly.
  • Event pages have a new design, using the Story layout until you choose another. Homepage sections you saved keep their order and visibility.
  • Regenerating a schedule deletes dates that no longer match the rule. Dates with orders or attendees are kept and marked as edited by hand.
  • End dates default to the end of the day. Picking only a date in an end or expiry field now sets 11:59 PM instead of midnight. Saved values don't change.
  • Stripe refund emails wait for Stripe. The buyer's refund email is sent once Stripe confirms the refund, not when it's requested, and isn't sent if it fails. Installs without the refund.created and refund.updated webhooks no longer send it. Offline and manual refunds still email straight away.
  • Test messages only go to you. "Send as test" emails only the sender for every recipient type, and sends immediately even if "Schedule for later" is selected.
  • Reactivating an attendee checks availability (ticket quantity, shared capacity and the date's capacity) and returns a 422 instead of overselling.
  • Products with waitlist_enabled unset block waitlist joins. Before, only an explicit false did. The storefront no longer offers the waitlist outside a price's sale window.
  • Ordering on unpublished events is limited to members of the owning account, and refused while the event is under review. Waitlist joins and contacting the organizer need a live event.
  • Draft previews in an embedded widget. A signed-in organizer no longer sees a draft event through a widget embedded on another site. Preview drafts in the app.
  • 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. Someone who changes their email before confirming the original needs "resend confirmation"; the original link no longer works.
  • Team invitations need a verified account. Someone who already has a Hi.Events login signs in and joins (409 INVITATION_SIGN_IN_REQUIRED) instead of setting a new password from the invitation. Account and sign-in emails no longer greet people by name.
  • 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. Organizers can change it in organizer settings.
  • Prices follow the app language, not the browser's locale.
  • Event messages are marked Sent once the email has 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 on an expired or cancelled order are still refunded and the buyer emailed, but no longer set the order's refund status, change statistics or send order.refunded.
  • 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. It does nothing until a PIN is set or an organizer opens it, and needs a licence to be used.
  • Google Tag Manager. On self-hosted installs, only account admins can add a new container, and API keys never can.
  • Links in organizer and event content are nofollow ugc, and widget pages (/widget/*) send X-Robots-Tag: noindex.
  • SaaS mode only: passing platform fees to the buyer defaults to on for new organizers (APP_SAAS_DEFAULT_PASS_PLATFORM_FEE_TO_BUYER=false keeps the old default), and overridden stripe_connect_return_url and stripe_connect_refresh_url values need a %d for the organizer id.

Since rc.3

For anyone already on a release candidate:

  • Redesigned event and organizer pages with Marquee, Flyer, Story and Invite layouts, a "More from this organizer" section, no layout shift on load, and accessibility and contrast fixes.
  • Switch an event between single and recurring from Event settings.
  • Duplicating an event keeps sale dates in non-UTC events, copies email templates, question descriptions and order, and category order, and has a reworked dialog.
  • Security hardening: purpose-bound emailed tokens, refusal of the example JWT_SECRET, shared capacity checked under the order lock, public check-in scoped to its event, stricter waitlist joins, an optional per-IP reservation cap, and every known dependency advisory cleared.
  • Self-hosting feedback from #1431: invited users stuck at login, orders from deleted events, iDEAL name prefill, the platform fees report, and the gap above the pay button.
  • Embedded checkout reports each order to the host page once, even if the checkout reloads.
  • Building the frontend from source needs Node 22.22 or higher.

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 recurring events, embedded checkout, 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.

Reserved seating

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

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 step 7 of the upgrade).

How licences work

  • Licence status, expiry and the features in use are shown in a Licence dialog in the avatar menu, and on a 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.
  • An invalid key is logged once a day rather than on every request.

What's Changed

The full list for each release candidate is in the rc.1, rc.2 and rc.3 notes. Since rc.3:

✨ Features

🐛 Fixes

🔧 Improvements

🧰 Development

Contributors

Thanks to everyone who tested the release candidates and filed issues, and to the contributors whose work is part of v2: @mrjbj, @officialJCReyes, @dalsoop, @Viburnum, @albanobattistella, @manaskumar3003, @b-pm, @zigman79, @cornelisonc, @Tt0x and @max-1-max.

Full Changelog: v1.11.1-beta...v2.0.0

Don't miss a new Hi.Events release

NewReleases is sending notifications on new releases.