Deploying Hi.Events

Hi.Events offers flexible deployment options for both cloud and self-hosted environments. Whether you’re looking for a quick setup or a fully customized configuration, this guide will help you get up and running.

For most users, our recommended approach is using Docker, which simplifies deployment and ensures consistency across different environments.

Overview

Hi.Events consists of two main components:

  • Frontend: A Node.js React application that handles the user interface
  • Backend: A Laravel PHP application that powers the API and business logic

You can deploy these components either:

  • Together using our all-in-one Docker image (simplest approach)
  • Separately for better scalability and control (recommended for production)

Deployment Options

One-Click Cloud Deployment

The fastest way to deploy Hi.Events is through our verified cloud partners:

Deploy on DigitalOcean

Deploy to Render

Deploy on Railway

Deploy on Zeabur

Deploy on Elestio
💡

Production Considerations

While one-click deployments are convenient for getting started, production environments require careful configuration of:

  • Server resources based on expected traffic
  • Database size and performance
  • Security settings and environment variables
  • Proper file storage configuration

Environment Variables

To configure your Hi.Events deployment properly, you’ll need to set up several environment variables. These control everything from database connections to email delivery settings.

Frontend Variables

Variable NameDescriptionExample
VITE_FRONTEND_URLFrontend URLhttps://your-app.com
VITE_API_URL_CLIENTAPI URL for use in the browserhttps://your-app.com/api
VITE_API_URL_SERVERAPI URL for use on serverThis is used for server-side rendering. In the All-in-one image this is fixed to http://localhost:80/api internally and cannot be overridden. If you’re hosting frontend and backend separately, this value would usually be the same as VITE_API_URL_CLIENT.
VITE_STRIPE_PUBLISHABLE_KEYStripe public keypk_test_51...
VITE_APP_NAMEApplication name shown in the browser title and throughout the frontend UI. Emails use the backend APP_NAME variableDefault: Hi.Events
NODE_PORTPort the frontend server listens on. Only relevant if you’re running the frontend image behind your own proxyDefault: 5678

Frontend Branding (Optional)

These are all optional — leave them unset to use the Hi.Events defaults.

Variable NameDescriptionExample
VITE_APP_LOGO_LIGHTLogo used on light backgroundshttps://your-app.com/logo-light.svg
VITE_APP_LOGO_DARKLogo used on dark backgroundshttps://your-app.com/logo-dark.svg
VITE_APP_FAVICONFavicon URLDefault: /favicon.svg
VITE_APP_PRIMARY_COLORPrimary brand colourDefault: #40296C
VITE_APP_SECONDARY_COLORSecondary brand colourDefault: #3d0b44
VITE_TOS_URLTerms of Service URL. Terms links are hidden at checkout when unsethttps://your-app.com/terms
VITE_PRIVACY_URLPrivacy Policy URLhttps://your-app.com/privacy
VITE_HIDE_ABOUT_LINKHides the “About” link in the global menu when settrue
VITE_PLATFORM_SUPPORT_EMAILSupport email surfaced in the UI[email protected]

Leave these unset to keep the default behaviour: no consent banner, and organizer tracking pixels load as soon as an event page opens.

Variable NameDescriptionExample
VITE_COOKIE_CONSENT_ENABLEDShows a site-wide cookie banner (Essential / Analytics / Advertising). Organizer tracking pixels and Google Consent Mode follow the visitor’s choicetrue
VITE_COOKIE_CONSENT_DOMAINShares the consent cookie across sub-domains. Only applied when it matches the host.your-app.com
VITE_COOKIE_CONSENT_TEXTOverrides the banner textWe use cookies to improve your experience.
VITE_GOOGLE_ADS_CONVERSION_IDGoogle Ads tag, loaded on every page under Consent ModeAW-123456789
VITE_GOOGLE_ADS_CONVERSION_LABELSMaps app events to Google Ads conversion labelssignup_completed:AbCdEfGh
VITE_FATHOM_SITE_IDFathom Analytics site ID (cookieless, not gated by the banner)ABCDEFGH

Backend Variables

Mail Configuration

You can use email providers like Postmark, SendGrid, or AWS SES.

Variable NameDescriptionExample
MAIL_MAILERMail driversmtp
MAIL_HOSTMail server hostsmtp.mailtrap.io
MAIL_PORTMail server port2525
MAIL_USERNAMEMail server usernameyour-username
MAIL_PASSWORDMail server passwordyour-password
MAIL_ENCRYPTIONSMTP encryption scheme. Set to null to disabletls
MAIL_FROM_ADDRESSMail from address[email protected]
MAIL_FROM_NAMEMail from nameYour App Name
MAIL_AUTO_TLSAutomatically negotiate TLS with the mail server. Set to false for local relays without TLSDefault: true
MAIL_VERIFY_PEERVerify the mail server’s TLS certificate. Set to false for self-signed certificates on local relaysDefault: true
API-based mail providers

The MAIL_HOST / MAIL_PORT / MAIL_USERNAME / MAIL_PASSWORD variables above only apply when MAIL_MAILER=smtp. If you’d rather use a provider’s API, set MAIL_MAILER to the provider name and supply its credentials instead:

ProviderMAIL_MAILERRequired variables
PostmarkpostmarkPOSTMARK_TOKEN
AWS SESsesAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION
MailgunmailgunMAILGUN_DOMAIN, MAILGUN_SECRET (and MAILGUN_ENDPOINT for the EU region)

Note

AWS SES reuses the same AWS_* credentials as S3 file storage. If you use different accounts for storage and email, use SMTP credentials for SES instead.

For more details on configuring mail settings in Laravel, refer to the Laravel Mail Documentation.

Stripe Configuration

For more information on obtaining Stripe API keys, visit the Stripe API Keys Documentation.

Variable NameDescriptionExample
STRIPE_PUBLIC_KEYStripe public keypk_test_51...
STRIPE_SECRET_KEYStripe secret keysk_test_51...
STRIPE_WEBHOOK_SECRETStripe webhook secretwhsec_...
Setting up the Stripe webhook

For Stripe to work correctly, you need to set up the webhook in your Stripe dashboard:

  1. Go to https://dashboard.stripe.com/webhooks
  2. Click “Add endpoint”
  3. Set the webhook URL to your backend’s Stripe webhook endpoint (see the note below): https://your-app.com/api/public/webhooks/stripe
  4. You should listen for the following events:
  • payment_intent.succeeded
  • payment_intent.payment_failed
  • charge.succeeded
  • charge.updated
  • charge.refunded
  • refund.created
  • refund.updated
  • account.updated
  • payout.paid
  • payout.updated
⚠️

The /api prefix is not part of the route

The Laravel route is /public/webhooks/stripe. The /api segment only exists in the all-in-one image, whose bundled nginx strips it before passing the request to the backend.

If you’re running the separate backend image or a manual install, the correct URL is https://your-backend-host/public/webhooks/stripe — unless you’ve put a proxy in front of it that strips /api the same way.

General Configuration

Variable NameDescriptionExample
APP_KEYApplication keybase64:...
APP_NAMEApplication name used in outgoing emailsDefault: Hi.Events
APP_URLThe backend’s own public URL. Used for Artisan-generated URLs and, when using local disk storage, to build the public /storage URLhttps://your-app.com
APP_FRONTEND_URLFrontend URLhttps://your-app.com
APP_CDN_URLBase URL that uploaded files are served from. See the note below — this is effectively required for local disk storagehttps://your-app.com/storage
APP_ENVApplication environmentDefault: production
APP_DEBUGShow detailed error messages and stack traces. Must be false in productionDefault: false
APP_DISABLE_REGISTRATIONDisable registrationDisables people from registering new accounts. Suggested for non-SaaS deployments.
APP_ENFORCE_EMAIL_CONFIRMATION_DURING_REGISTRATIONRequire new users to confirm their email address before they can use the appDefault: false
APP_PLATFORM_SUPPORT_EMAILSupport address shown in user-facing emails. Change this — the default is a placeholderDefault: [email protected]
APP_EMAIL_LOGO_URLLogo displayed at the top of outgoing emailshttps://your-app.com/logo.png
APP_EMAIL_LOGO_LINK_URLWhere the email logo links toDefaults to APP_FRONTEND_URL
APP_EMAIL_FOOTER_TEXTCustom footer text appended to outgoing emails© Your Company
APP_API_RATE_LIMIT_PER_MINUTEAPI requests allowed per minuteDefault: 180
APP_ALLOWED_INTERNAL_WEBHOOK_HOSTSComma-separated hosts that outgoing webhooks may target on private/internal networks. Empty by default, which blocks internal hostsinternal.your-app.com
FILESYSTEM_PUBLIC_DISKFilesystem diskDefault: s3-public. public if you’re using local disk storage
FILESYSTEM_PRIVATE_DISKFilesystem diskDefault: s3-private. local if you’re using local disk storage
JWT_SECRETJWT secret key. A plain random string — no base64: prefixYy2f... (see below)
LOG_CHANNELLog channel. See Error Reporting and Logsstderr
CORS_ALLOWED_ORIGINSA comma-separated list of allowed origins for CORS requests. Enter ’*’ to allow all origins.https://your-app.com,https://another-origin.com
⚠️

No spaces in CORS_ALLOWED_ORIGINS

Separate multiple origins with commas and no spaces — a space after the comma becomes part of the origin, and that origin won’t match.

Generate the APP_KEY using:

echo "base64:$(openssl rand -base64 32)"

Generate the JWT_SECRET using — note there is no base64: prefix here, unlike APP_KEY:

openssl rand -base64 32

APP_CDN_URL and local disk storage

APP_CDN_URL isn’t only for CDNs — it’s the base URL prepended to every uploaded file path (event cover images, organizer logos, and so on).

If you’re using local disk storage (FILESYSTEM_PUBLIC_DISK=public), set this to your app’s public storage path, for example APP_CDN_URL=https://your-app.com/storage. If it’s left unset, Hi.Events falls back to the public disk’s URL, which is built from APP_URL — so APP_URL must be correct or uploaded images will render as broken links.

Error Reporting and Logs (Optional)

Hi.Events can report errors — and optionally forward log records — to Sentry from both the backend and the frontend’s server-side rendering process. Leave the DSNs unset to disable it entirely.

Backend:

Variable NameDescriptionExample
SENTRY_DSNSentry DSN for the backend. Unset disables Sentryhttps://[email protected]/...
SENTRY_ENVIRONMENTEnvironment name reported with each event. Set this per deployment so staging and production stay separateproduction
SENTRY_ENABLE_LOGSSend log records to Sentry Logs, not just exceptionsDefault: false
SENTRY_LOG_LEVELMinimum level forwarded when logs are enabledDefault: info
SENTRY_RELEASERelease identifier, so errors can be tied to a deployv2.3.0
SENTRY_TRACES_SAMPLE_RATEFraction of requests traced for performance monitoring. 0 disables tracingDefault: 0
LOG_STACKComma-separated channels the stack log channel writes to. Add sentry_logs to forward logs to SentryDefault: single. e.g. stderr,sentry_logs

Frontend (the SSR server — these are read by frontend/server.js, not the browser, so they have no VITE_ prefix):

Variable NameDescriptionExample
SENTRY_SSR_DSNSentry DSN for the SSR server. Unset disables Sentry therehttps://[email protected]/...
SENTRY_ENVIRONMENTEnvironment name. Falls back to NODE_ENV with a warningproduction
SENTRY_ENABLE_LOGSForward server console output to Sentry LogsDefault: false
SENTRY_LOG_LEVELMinimum level forwarded when logs are enabledDefault: info
SENTRY_RELEASERelease identifierv2.3.0
SENTRY_TRACES_SAMPLE_RATEFraction of requests traced. 0 disables tracingDefault: 0

Turning on Sentry Logs

Setting SENTRY_ENABLE_LOGS=true on the backend is not enough on its own — the sentry_logs channel also has to be in the stack that receives your logs. Set LOG_CHANNEL=stack and LOG_STACK=stderr,sentry_logs.

The SSR server has no such stack: SENTRY_ENABLE_LOGS=true there captures console output directly.

The SSR integration is configured to send as little as possible — user info, cookies, headers, request bodies, and query strings are all excluded.

Address Autocomplete (Optional)

Hi.Events v2 supports venue address autocomplete powered by Google Places. This is optional — without it, organizers can still enter venue addresses manually.

Variable NameDescriptionExample
GEO_PROVIDERGeocoding provider for address autocompletegoogle
GOOGLE_MAPS_API_KEYGoogle Maps API key with the Places API enabledAIza...

SaaS Configuration

Note

These variables are only relevant if you are using the SaaS version of Hi.Events.

Variable NameDescriptionExample
APP_SAAS_MODE_ENABLEDEnable SaaS mode (Defaults to falsetrue
APP_SAAS_STRIPE_APPLICATION_FEE_PERCENTStripe application fee percentage. Only relevant in SAAS mode1.5 for 1.5%
APP_SAAS_STRIPE_APPLICATION_FEE_FIXEDStripe application fee fixed. Only relevant in SAAS mode.40 for 40c
APP_SAAS_DEFAULT_PASS_PLATFORM_FEE_TO_BUYERWhether platform fees are passed on to the ticket buyer by defaultDefault: true
APP_STRIPE_CONNECT_ACCOUNT_TYPEStripe Connect account type used when organizers connect StripeDefault: express
OPEN_EXCHANGE_RATES_APP_IDOpen Exchange Rates App ID for currency conversionyour-app-id

The SAAS fee variables seed the initial default platform fee configuration. They are not read on every order, so changing them later has no effect on existing organizers.

At checkout, the fee rates are read from the organizer’s assigned configuration (organizer_configurations.application_fees), which every new organizer inherits from the system default.

To change the rates after deployment, update the application_fees values on the relevant organizer_configurations row. Editing the system default row changes the rates inherited by organizers still using it.

The Platform Fees sections in Organizer Settings and Event Settings do not set rates — they only control who pays the fee. Organizer Settings sets the default for newly created events; Event Settings overrides that for a single event.

Event Moderation (Optional)

Hi.Events can run newly published events through an automated spam check. When an event is flagged, it’s moved to a PENDING_MANUAL_REVIEW status — hidden from the public and locked from status changes — the organizer is emailed, and a notification goes to APP_PLATFORM_SUPPORT_EMAIL for an admin to approve or confirm from the admin area.

Variable NameDescriptionExample
APP_EVENT_SPAM_CHECK_ENABLEDEnable the automated checkDefault: false
APP_EVENT_SPAM_CHECK_CONFIDENCE_THRESHOLDMinimum confidence before an event is flagged. Raise it to flag lessDefault: 0.7
ANTHROPIC_API_KEYAPI key used for the checksk-ant-...
⚠️

All three conditions are required

The check only runs when SaaS mode is on (APP_SAAS_MODE_ENABLED=true), APP_EVENT_SPAM_CHECK_ENABLED=true, and ANTHROPIC_API_KEY is set. If any one is missing, events publish as normal with no check.

The check runs as a queued job, so it also needs a queue worker — with QUEUE_CONNECTION=sync it runs inline and slows down publishing.

To re-run the check across existing live events — after changing the threshold, for example — use:

php artisan events:recheck-spam

AWS Configuration

These variables are required if you’d like to use AWS S3 for file storage. You can also use other s3-compatible services like DigitalOcean Spaces.

Production note

To avoid losing files during updates or server failures, we highly recommend using cloud file storage for production deployments.

Variable NameDescriptionExample
AWS_ACCESS_KEY_IDAWS access key IDyour-access-key-id
AWS_SECRET_ACCESS_KEYAWS secret access keyyour-secret-access-key
AWS_DEFAULT_REGIONAWS regionus-west-1
AWS_PUBLIC_BUCKETAWS public bucket nameyour-public-bucket
AWS_PRIVATE_BUCKETAWS private bucket nameyour-private-bucket
AWS_ENDPOINTCustom S3 API endpoint. Required for any non-AWS S3-compatible provider — without it the AWS SDK talks to real AWS S3https://nyc3.digitaloceanspaces.com
AWS_USE_PATH_STYLE_ENDPOINTUse path-style bucket URLs (endpoint/bucket/key) instead of virtual-hosted style. Required by MinIO and some other providersDefault: false
AWS_URLPublic base URL that stored files are served from. Set this if your files are served from a CDN or custom domain rather than the bucket endpointhttps://cdn.your-app.com

Database Configuration

You can either set individual database configuration variables or use the DATABASE_URL to simplify the configuration.

Variable NameDescriptionExample
DB_CONNECTIONDatabase connection typepgsql
DB_HOSTDatabase hostyour-database-host
DB_PORTDatabase port5432
DB_DATABASEDatabase nameyour-database-name
DB_USERNAMEDatabase usernameyour-database-username
DB_PASSWORDDatabase passwordyour-database-password
DATABASE_URLDatabase URL (alternative to individual values)postgres://user:password@host:port/database

Redis Configuration

Variable NameDescriptionExample
REDIS_HOSTRedis hostyour-redis-host
REDIS_PASSWORDRedis passwordyour-redis-password
REDIS_USERNAMERedis usernameyour-redis-username
REDIS_PORTRedis port6379
REDIS_URLRedis URLredis://user:password@host:port

Queue Configuration

Variable NameDescriptionExample
QUEUE_CONNECTIONQueue connection typeDefault: sync. Set to redis for production deployments.
WEBHOOK_QUEUE_NAMEName of the queue that outgoing webhook jobs are dispatched to. Required whenever QUEUE_CONNECTION is not syncwebhook-queue
OCCURRENCES_QUEUE_NAMEName of the queue that recurring-event occurrence generation jobs are dispatched to. Falls back to the default queue when unsetoccurrences
🚫

Set WEBHOOK_QUEUE_NAME when using a queue

Whenever QUEUE_CONNECTION is anything other than sync, set WEBHOOK_QUEUE_NAME=webhook-queue and make sure your worker consumes that queue (see Running a Queue Worker). Webhooks are not delivered if it is left unset.

Production note

For convenience, QUEUE_CONNECTION is set to sync by default. It is highly recommended to use a queue system like Redis for production deployments.

The QUEUE_CONNECTION=redis and WEBHOOK_QUEUE_NAME=webhook-queue values come from the provided docker-compose.yml and its .env.example, not from the all-in-one image itself, and the Redis server is a separate compose service. If you run the all-in-one image directly (docker run, Kubernetes, a PaaS) rather than via that compose file, you must supply these environment variables yourself and point REDIS_HOST at a reachable Redis instance.

Cache and Session Configuration

Variable NameDescriptionExample
CACHE_DRIVERCache store driverDefault: file. Set to redis for production deployments.
SESSION_DRIVERSession driverDefault: file. Set to redis for production deployments.
⚠️

Use redis if you run more than one backend instance

Both default to file, which stores data on each container’s own filesystem. Stripe webhook idempotency is enforced through the cache, so with the file driver and multiple backend replicas the same Stripe event can be processed more than once — potentially producing duplicate orders or refunds. Homepage ticket-quantity caching is also per-instance under file.

For any deployment running more than one backend instance, set CACHE_DRIVER=redis (and SESSION_DRIVER=redis).

Database Setup and Migrations

Hi.Events won’t start until its database schema has been created. Migrations must also be re-run after every upgrade.

The all-in-one Docker image handles this for you: its startup script runs php artisan migrate --force (and aborts startup if migrations fail) followed by php artisan storage:link on every container start. No action is needed.

For separate image deployments and manual setups you must run these yourself. The hi.events-backend image does not migrate on its own by default. (It is built on serversideup/php, so setting AUTORUN_ENABLED=true enables that base image’s opt-in automations, including migrations — but don’t rely on this unless you’ve configured it deliberately.)

Once your database environment variables are configured, run from the backend directory:

php artisan migrate --force

Then, only if you’re using local disk storage (FILESYSTEM_PUBLIC_DISK=public), create the symlink that makes uploaded files publicly reachable:

php artisan storage:link

If you’re running the backend in a Docker container, run these via docker exec:

docker exec your-backend-container php artisan migrate --force
docker exec your-backend-container php artisan storage:link
⚠️

Run migrations on every upgrade

Deploying a new version of the backend image without re-running php artisan migrate --force will leave the application running against an out-of-date schema. Make it part of your deploy process — for example as an init container, a release-phase command, or a one-off job that runs before the new backend pods start serving traffic.

--force is required because migrations are otherwise refused when APP_ENV=production.

Running a Queue Worker

If you set QUEUE_CONNECTION to anything other than sync, a queue worker must be running or queued work — emails, webhooks, and occurrence generation for recurring events — will never be processed.

The all-in-one Docker image runs a worker automatically via Supervisor. For separate image deployments or manual setups, run:

php artisan queue:work --queue=default,webhook-queue,occurrences --sleep=3 --tries=3 --timeout=60
⚠️

Queue names must match your configuration

The queues listed after --queue= must match the values of WEBHOOK_QUEUE_NAME and OCCURRENCES_QUEUE_NAME. The command above assumes WEBHOOK_QUEUE_NAME=webhook-queue and OCCURRENCES_QUEUE_NAME=occurrences. If those variables are unset, jobs will be dispatched to queue names your worker isn’t listening on and will never run.

Keep this process alive with a process manager such as Supervisor or systemd. See the Laravel Queues documentation for details.

Running the Scheduler

Hi.Events uses Laravel’s task scheduler for recurring background tasks. If the scheduler isn’t running, these features won’t work:

TaskFrequencyWhat breaks without the scheduler
Scheduled messagesEvery minuteMessages scheduled for a future time are never sent
Expired waitlist offersEvery minuteWaitlist offers never expire, so the released capacity is never offered to the next person in line
Scheduled account deletionsHourlyAccount deletions requested through Danger Zone never complete after their 30-day grace period
Failed jobs monitorEvery 5 minutesNo warning is logged when jobs start failing and piling up in the queue

The all-in-one Docker image (daveearley/hi.events-all-in-one) runs the scheduler automatically — no extra setup needed.

For separate image deployments or manual setups, you need to add a single cron entry on the server running your backend. This cron job calls Laravel’s schedule:run command every minute, and Laravel decides internally which scheduled tasks are due:

* * * * * cd /path-to-your-backend && php artisan schedule:run >> /dev/null 2>&1

Replace /path-to-your-backend with the actual path to your Hi.Events backend directory. If you’re running the backend in a Docker container, you can add this to the container’s crontab or run it via docker exec:

* * * * * docker exec your-backend-container php artisan schedule:run >> /dev/null 2>&1

You can verify the scheduler is working by running php artisan schedule:list — this shows all registered scheduled tasks and when they’re next due.

For more details on Laravel’s scheduler (daemon mode, running without cron, etc.), see the Laravel Scheduling documentation.

Important Deployment Considerations

⚠️

Common Issues

  • Environment variable misconfiguration is the most common cause of deployment problems
  • Performance issues typically stem from underpowered hardware or incorrectly configured queues
  • In most cloud environments, the filesystem is ephemeral - uploaded files will be lost on redeploy unless you use cloud storage

Checklist Before Going Live

  • All environment variables are properly configured
  • Queue system is set up according to
  • File storage solution is configured (local vs cloud storage)
  • SSL certificates are installed and valid
  • Backups are configured
  • Monitoring is in place

If you’re planning to host events that might experience sudden bursts of traffic (like popular ticket sales or flash sales), you’ll want to take extra precautions with your deployment:

Tips for High Traffic Events

When expecting high traffic:

  • Ensure you have queues set up for background processing. Do not set QUEUE_CONNECTION to sync.
  • Use a more powerful database instance, as this is typically the bottleneck
  • Consider using a CDN for static assets
  • Monitor your infrastructure metrics during the event
  • Have a plan in place for scaling up if needed
  • Consider using a managed database service for better scalability
  • Use a load balancer to distribute traffic