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:
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 Name | Description | Example |
|---|---|---|
VITE_FRONTEND_URL | Frontend URL | https://your-app.com |
VITE_API_URL_CLIENT | API URL for use in the browser | https://your-app.com/api |
VITE_API_URL_SERVER | API URL for use on server | This 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_KEY | Stripe public key | pk_test_51... |
VITE_APP_NAME | Application name shown in the browser title and throughout the frontend UI. Emails use the backend APP_NAME variable | Default: Hi.Events |
NODE_PORT | Port the frontend server listens on. Only relevant if you’re running the frontend image behind your own proxy | Default: 5678 |
Frontend Branding (Optional)
These are all optional — leave them unset to use the Hi.Events defaults.
| Variable Name | Description | Example |
|---|---|---|
VITE_APP_LOGO_LIGHT | Logo used on light backgrounds | https://your-app.com/logo-light.svg |
VITE_APP_LOGO_DARK | Logo used on dark backgrounds | https://your-app.com/logo-dark.svg |
VITE_APP_FAVICON | Favicon URL | Default: /favicon.svg |
VITE_APP_PRIMARY_COLOR | Primary brand colour | Default: #40296C |
VITE_APP_SECONDARY_COLOR | Secondary brand colour | Default: #3d0b44 |
VITE_TOS_URL | Terms of Service URL. Terms links are hidden at checkout when unset | https://your-app.com/terms |
VITE_PRIVACY_URL | Privacy Policy URL | https://your-app.com/privacy |
VITE_HIDE_ABOUT_LINK | Hides the “About” link in the global menu when set | true |
VITE_PLATFORM_SUPPORT_EMAIL | Support email surfaced in the UI | [email protected] |
Cookie Consent & Analytics (Optional)
Leave these unset to keep the default behaviour: no consent banner, and organizer tracking pixels load as soon as an event page opens.
| Variable Name | Description | Example |
|---|---|---|
VITE_COOKIE_CONSENT_ENABLED | Shows a site-wide cookie banner (Essential / Analytics / Advertising). Organizer tracking pixels and Google Consent Mode follow the visitor’s choice | true |
VITE_COOKIE_CONSENT_DOMAIN | Shares the consent cookie across sub-domains. Only applied when it matches the host | .your-app.com |
VITE_COOKIE_CONSENT_TEXT | Overrides the banner text | We use cookies to improve your experience. |
VITE_GOOGLE_ADS_CONVERSION_ID | Google Ads tag, loaded on every page under Consent Mode | AW-123456789 |
VITE_GOOGLE_ADS_CONVERSION_LABELS | Maps app events to Google Ads conversion labels | signup_completed:AbCdEfGh |
VITE_FATHOM_SITE_ID | Fathom 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 Name | Description | Example |
|---|---|---|
MAIL_MAILER | Mail driver | smtp |
MAIL_HOST | Mail server host | smtp.mailtrap.io |
MAIL_PORT | Mail server port | 2525 |
MAIL_USERNAME | Mail server username | your-username |
MAIL_PASSWORD | Mail server password | your-password |
MAIL_ENCRYPTION | SMTP encryption scheme. Set to null to disable | tls |
MAIL_FROM_ADDRESS | Mail from address | [email protected] |
MAIL_FROM_NAME | Mail from name | Your App Name |
MAIL_AUTO_TLS | Automatically negotiate TLS with the mail server. Set to false for local relays without TLS | Default: true |
MAIL_VERIFY_PEER | Verify the mail server’s TLS certificate. Set to false for self-signed certificates on local relays | Default: 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:
| Provider | MAIL_MAILER | Required variables |
|---|---|---|
| Postmark | postmark | POSTMARK_TOKEN |
| AWS SES | ses | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION |
| Mailgun | mailgun | MAILGUN_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 Name | Description | Example |
|---|---|---|
STRIPE_PUBLIC_KEY | Stripe public key | pk_test_51... |
STRIPE_SECRET_KEY | Stripe secret key | sk_test_51... |
STRIPE_WEBHOOK_SECRET | Stripe webhook secret | whsec_... |
Setting up the Stripe webhook
For Stripe to work correctly, you need to set up the webhook in your Stripe dashboard:
- Go to https://dashboard.stripe.com/webhooks
- Click “Add endpoint”
- Set the webhook URL to your backend’s Stripe webhook endpoint (see the note below):
https://your-app.com/api/public/webhooks/stripe - You should listen for the following events:
payment_intent.succeededpayment_intent.payment_failedcharge.succeededcharge.updatedcharge.refundedrefund.createdrefund.updatedaccount.updatedpayout.paidpayout.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 Name | Description | Example |
|---|---|---|
APP_KEY | Application key | base64:... |
APP_NAME | Application name used in outgoing emails | Default: Hi.Events |
APP_URL | The backend’s own public URL. Used for Artisan-generated URLs and, when using local disk storage, to build the public /storage URL | https://your-app.com |
APP_FRONTEND_URL | Frontend URL | https://your-app.com |
APP_CDN_URL | Base URL that uploaded files are served from. See the note below — this is effectively required for local disk storage | https://your-app.com/storage |
APP_ENV | Application environment | Default: production |
APP_DEBUG | Show detailed error messages and stack traces. Must be false in production | Default: false |
APP_DISABLE_REGISTRATION | Disable registration | Disables people from registering new accounts. Suggested for non-SaaS deployments. |
APP_ENFORCE_EMAIL_CONFIRMATION_DURING_REGISTRATION | Require new users to confirm their email address before they can use the app | Default: false |
APP_PLATFORM_SUPPORT_EMAIL | Support address shown in user-facing emails. Change this — the default is a placeholder | Default: [email protected] |
APP_EMAIL_LOGO_URL | Logo displayed at the top of outgoing emails | https://your-app.com/logo.png |
APP_EMAIL_LOGO_LINK_URL | Where the email logo links to | Defaults to APP_FRONTEND_URL |
APP_EMAIL_FOOTER_TEXT | Custom footer text appended to outgoing emails | © Your Company |
APP_API_RATE_LIMIT_PER_MINUTE | API requests allowed per minute | Default: 180 |
APP_ALLOWED_INTERNAL_WEBHOOK_HOSTS | Comma-separated hosts that outgoing webhooks may target on private/internal networks. Empty by default, which blocks internal hosts | internal.your-app.com |
FILESYSTEM_PUBLIC_DISK | Filesystem disk | Default: s3-public. public if you’re using local disk storage |
FILESYSTEM_PRIVATE_DISK | Filesystem disk | Default: s3-private. local if you’re using local disk storage |
JWT_SECRET | JWT secret key. A plain random string — no base64: prefix | Yy2f... (see below) |
LOG_CHANNEL | Log channel. See Error Reporting and Logs | stderr |
CORS_ALLOWED_ORIGINS | A 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 32APP_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 Name | Description | Example |
|---|---|---|
SENTRY_DSN | Sentry DSN for the backend. Unset disables Sentry | https://[email protected]/... |
SENTRY_ENVIRONMENT | Environment name reported with each event. Set this per deployment so staging and production stay separate | production |
SENTRY_ENABLE_LOGS | Send log records to Sentry Logs, not just exceptions | Default: false |
SENTRY_LOG_LEVEL | Minimum level forwarded when logs are enabled | Default: info |
SENTRY_RELEASE | Release identifier, so errors can be tied to a deploy | v2.3.0 |
SENTRY_TRACES_SAMPLE_RATE | Fraction of requests traced for performance monitoring. 0 disables tracing | Default: 0 |
LOG_STACK | Comma-separated channels the stack log channel writes to. Add sentry_logs to forward logs to Sentry | Default: 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 Name | Description | Example |
|---|---|---|
SENTRY_SSR_DSN | Sentry DSN for the SSR server. Unset disables Sentry there | https://[email protected]/... |
SENTRY_ENVIRONMENT | Environment name. Falls back to NODE_ENV with a warning | production |
SENTRY_ENABLE_LOGS | Forward server console output to Sentry Logs | Default: false |
SENTRY_LOG_LEVEL | Minimum level forwarded when logs are enabled | Default: info |
SENTRY_RELEASE | Release identifier | v2.3.0 |
SENTRY_TRACES_SAMPLE_RATE | Fraction of requests traced. 0 disables tracing | Default: 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 Name | Description | Example |
|---|---|---|
GEO_PROVIDER | Geocoding provider for address autocomplete | google |
GOOGLE_MAPS_API_KEY | Google Maps API key with the Places API enabled | AIza... |
SaaS Configuration
Note
These variables are only relevant if you are using the SaaS version of Hi.Events.
| Variable Name | Description | Example |
|---|---|---|
APP_SAAS_MODE_ENABLED | Enable SaaS mode (Defaults to false | true |
APP_SAAS_STRIPE_APPLICATION_FEE_PERCENT | Stripe application fee percentage. Only relevant in SAAS mode | 1.5 for 1.5% |
APP_SAAS_STRIPE_APPLICATION_FEE_FIXED | Stripe application fee fixed. Only relevant in SAAS mode | .40 for 40c |
APP_SAAS_DEFAULT_PASS_PLATFORM_FEE_TO_BUYER | Whether platform fees are passed on to the ticket buyer by default | Default: true |
APP_STRIPE_CONNECT_ACCOUNT_TYPE | Stripe Connect account type used when organizers connect Stripe | Default: express |
OPEN_EXCHANGE_RATES_APP_ID | Open Exchange Rates App ID for currency conversion | your-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 Name | Description | Example |
|---|---|---|
APP_EVENT_SPAM_CHECK_ENABLED | Enable the automated check | Default: false |
APP_EVENT_SPAM_CHECK_CONFIDENCE_THRESHOLD | Minimum confidence before an event is flagged. Raise it to flag less | Default: 0.7 |
ANTHROPIC_API_KEY | API key used for the check | sk-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-spamAWS 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 Name | Description | Example |
|---|---|---|
AWS_ACCESS_KEY_ID | AWS access key ID | your-access-key-id |
AWS_SECRET_ACCESS_KEY | AWS secret access key | your-secret-access-key |
AWS_DEFAULT_REGION | AWS region | us-west-1 |
AWS_PUBLIC_BUCKET | AWS public bucket name | your-public-bucket |
AWS_PRIVATE_BUCKET | AWS private bucket name | your-private-bucket |
AWS_ENDPOINT | Custom S3 API endpoint. Required for any non-AWS S3-compatible provider — without it the AWS SDK talks to real AWS S3 | https://nyc3.digitaloceanspaces.com |
AWS_USE_PATH_STYLE_ENDPOINT | Use path-style bucket URLs (endpoint/bucket/key) instead of virtual-hosted style. Required by MinIO and some other providers | Default: false |
AWS_URL | Public 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 endpoint | https://cdn.your-app.com |
Database Configuration
You can either set individual database configuration variables or use the DATABASE_URL to simplify the configuration.
| Variable Name | Description | Example |
|---|---|---|
DB_CONNECTION | Database connection type | pgsql |
DB_HOST | Database host | your-database-host |
DB_PORT | Database port | 5432 |
DB_DATABASE | Database name | your-database-name |
DB_USERNAME | Database username | your-database-username |
DB_PASSWORD | Database password | your-database-password |
DATABASE_URL | Database URL (alternative to individual values) | postgres://user:password@host:port/database |
Redis Configuration
| Variable Name | Description | Example |
|---|---|---|
REDIS_HOST | Redis host | your-redis-host |
REDIS_PASSWORD | Redis password | your-redis-password |
REDIS_USERNAME | Redis username | your-redis-username |
REDIS_PORT | Redis port | 6379 |
REDIS_URL | Redis URL | redis://user:password@host:port |
Queue Configuration
| Variable Name | Description | Example |
|---|---|---|
QUEUE_CONNECTION | Queue connection type | Default: sync. Set to redis for production deployments. |
WEBHOOK_QUEUE_NAME | Name of the queue that outgoing webhook jobs are dispatched to. Required whenever QUEUE_CONNECTION is not sync | webhook-queue |
OCCURRENCES_QUEUE_NAME | Name of the queue that recurring-event occurrence generation jobs are dispatched to. Falls back to the default queue when unset | occurrences |
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 Name | Description | Example |
|---|---|---|
CACHE_DRIVER | Cache store driver | Default: file. Set to redis for production deployments. |
SESSION_DRIVER | Session driver | Default: 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 --forceThen, only if you’re using local disk storage (FILESYSTEM_PUBLIC_DISK=public), create the symlink that makes
uploaded files publicly reachable:
php artisan storage:linkIf 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:linkRun 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=60Queue 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:
| Task | Frequency | What breaks without the scheduler |
|---|---|---|
| Scheduled messages | Every minute | Messages scheduled for a future time are never sent |
| Expired waitlist offers | Every minute | Waitlist offers never expire, so the released capacity is never offered to the next person in line |
| Scheduled account deletions | Hourly | Account deletions requested through Danger Zone never complete after their 30-day grace period |
| Failed jobs monitor | Every 5 minutes | No 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>&1Replace /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>&1You 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