Ir al contenido principal

SaaS Setup

Configure Event Schedule for SaaS (Software as a Service) deployment, where you host the platform for multiple customers using subdomains.

Overview

Event Schedule supports two deployment modes:

Mode Routing Use Case
Selfhosted Path-based /schedule-name/... Single organization or personal use
SaaS/Hosted Subdomain-based schedule-name.yourdomain.com Multi-tenant platform for multiple customers

In SaaS mode each customer schedule gets its own subdomain, and signing in, the admin portal and billing all live on one shared app subdomain. A schedule on an Enterprise plan can additionally be served from the customer's own domain; see Custom Domains.

Your platform does not serve the Event Schedule marketing site

The marketing pages (home, features, pricing, this user guide) are registered only when IS_NEXUS=true, which identifies the one upstream install that receives federated events and shared translations. Leave it unset on your own platform. Your root domain then redirects visitors to the sign-in page, and you point APP_MARKETING_URL at whatever marketing site you run yourself.

Prerequisites

  1. A completed base installation of Event Schedule, including MySQL and the schedule:run cron entry (see Installation)
  2. A domain name with DNS access
  3. Ability to configure wildcard SSL certificates
  4. Web server configured to handle wildcard subdomains (Apache or Nginx)
  5. A working mail transport: tenant invitations, ticket confirmations, subscription receipts and support notifications all send from this install

Environment Configuration

Add the following variables to your .env file to enable SaaS mode:

Core SaaS Settings

.env
# Enable SaaS mode with subdomain routing
IS_HOSTED=true

# Sender name on outgoing email, via MAIL_FROM_NAME="${APP_NAME}"
APP_NAME=Your Platform Name

# Main application URL (use app subdomain)
APP_URL=https://app.yourdomain.com

# Marketing site URL (can be same as APP_URL)
APP_MARKETING_URL=https://yourdomain.com
Variable Default Description
IS_HOSTED false Enable subdomain-based routing for multi-tenant SaaS
APP_NAME Laravel Reaches the app only through the MAIL_FROM_NAME="${APP_NAME}" reference in .env.example, so it sets the sender name on outgoing email. It does not rename the product in the interface: admin and marketing page titles are literal, and config('app.name') is a fixed Event Schedule string in config/app.php. Public schedule pages are already unbranded, since their title carries the schedule's own name. Rename in-app wording with custom translations instead.
APP_URL - Application URL. Set to the app subdomain (e.g. https://app.yourdomain.com). The base domain is derived by stripping a leading app., www., blog. or demo., and the blog and demo subdomains are then built back from it automatically.
APP_MARKETING_URL https://eventschedule.com Your own marketing site. This is where the footer strip on your free tier's public pages sends visitors, so point it at your site rather than leaving the default.
IS_NEXUS false Leave this off. It marks the single upstream install that hosts the Event Schedule marketing site and receives federated events and shared translation suggestions. Turning it on also changes the default proxy trust and disables the in-app updater.

Branding Customization

One credit a page

Your app name, logos and domain make the platform yours, and your free tier's footer strip points at your APP_MARKETING_URL rather than ours. One thing is not yours to repoint: a small "Event Schedule" chip in the corner of the public pages of every customer you charge. It is the attribution the Attribution Assurance License asks for in return for the software, so it links to eventschedule.com and APP_MARKETING_URL does not change it. A free schedule shows your footer strip instead of the chip, so no page carries two credits.

.env
# Logo for light backgrounds (header, emails)
APP_LOGO_DARK=/images/dark_logo.png

# Logo for dark backgrounds (dark mode, footers)
APP_LOGO_LIGHT=/images/light_logo.png
Variable Default Description
APP_LOGO_DARK /images/dark_logo.png Logo displayed on light backgrounds
APP_LOGO_LIGHT /images/light_logo.png Logo displayed on dark backgrounds
Logo Guidelines
  • Place logo files in public/images/
  • Recommended dimensions: 200px width, transparent background
  • Supported formats: PNG, SVG
  • The dark logo should have dark/black text (for light backgrounds)
  • The light logo should have light/white text (for dark backgrounds)

Support Configuration

.env
# Email address for user feedback (displayed in footer)
SUPPORT_EMAIL=contact@eventschedule.com
Variable Default Description
SUPPORT_EMAIL contact@eventschedule.com Shown at the bottom of the admin sidebar as the "questions or suggestions" address, and used as the Reply-To on the notices sent when an account, schedule or event is deleted. Change it or your customers will write to us.

Pricing and Trial Configuration

.env
# Free trial length in days for new Pro subscribers
TRIAL_DAYS=7
Variable Default Description
TRIAL_DAYS 7 Length of the Stripe trial granted when a schedule subscribes for the first time. The shipped .env.example sets 365, so set it deliberately.
How Trials Work
  • A new schedule starts on the Free plan. Nothing grants it Pro automatically, so the free tier is what every customer sees first
  • The trial is applied at subscribe time: a schedule that has never had a plan or a subscription gets TRIAL_DAYS before Stripe takes the first payment, and the subscribe page shows a free-trial badge
  • A schedule carrying a legacy expiry date instead gets its remaining days as the trial length
  • Amounts are defined by the Price objects in your Stripe dashboard; the app only stores the Price IDs, plus separate display amounts (see Stripe Subscription Setup)

Push Notifications (Optional)

Event Schedule can send web push notifications that mirror its email notifications using OneSignal. This is a Pro feature and is off by default: with no configuration, no push SDK loads and no calls are made to OneSignal. To enable it platform-wide, create a OneSignal app (Web platform) and set:

ONESIGNAL_APP_ID=your-onesignal-app-id
ONESIGNAL_REST_API_KEY=your-onesignal-rest-api-key

Once both values are set, a Push notifications panel appears on each schedule's Settings → Notifications tab, where the owner enables push per device and can send a test. Sending is gated on the schedule being Pro or Enterprise, and the demo schedule never receives push. One OneSignal app serves the whole platform; tenants are segmented automatically. Add ONESIGNAL_SAFARI_WEB_ID only if you need legacy macOS Safari support.

Note that enabling push loads the OneSignal SDK from their CDN and sends notification data to OneSignal, and that Apple iOS only supports web push for sites added to the home screen (iOS 16.4+).

Running Behind a Reverse Proxy

A multi-tenant install almost always sits behind a reverse proxy or CDN (Nginx, Apache, Cloudflare, or a control panel such as HestiaCP). Tell Event Schedule which proxies to trust so it reads the X-Forwarded-Proto and X-Forwarded-For headers those proxies set:

TRUSTED_PROXIES=*

Use * to trust any proxy, or a comma-separated list of proxy IPs or CIDR ranges (for example 10.0.0.0/8,192.168.1.1) when the origin server is reachable directly from the internet. Left unset, your platform trusts no proxies at all: the application then treats every request as plain HTTP even when the browser is on HTTPS, which can produce redirect loops on tenant subdomains, and it records the proxy's IP address as the visitor's IP in analytics and rate limiting.

The setting deliberately lives in config/trustedproxy.php rather than in application bootstrap, so it survives php artisan config:cache. Re-run that command after changing the value.

DNS Configuration

For SaaS mode to work, you need to configure wildcard DNS records.

DNS Records

Add the following DNS records to your domain:

DNS (A Records)
# A record for main domain
yourdomain.com.    A    YOUR_SERVER_IP

# Wildcard A record for subdomains
*.yourdomain.com.  A    YOUR_SERVER_IP

Or if using a CNAME:

DNS (CNAME Records)
# CNAME for main domain
yourdomain.com.    CNAME    your-server.hosting.com.

# Wildcard CNAME for subdomains
*.yourdomain.com.  CNAME    your-server.hosting.com.

SSL Certificate

You'll need a wildcard SSL certificate that covers both the main domain and all subdomains:

  • Certificate should cover: yourdomain.com and *.yourdomain.com
  • Let's Encrypt supports wildcard certificates via DNS-01 challenge
  • Many hosting providers offer wildcard certificates

Web Server Configuration

Nginx Example

nginx.conf
server {
listen 443 ssl http2;
server_name yourdomain.com *.yourdomain.com;

ssl_certificate /path/to/wildcard.crt;
ssl_certificate_key /path/to/wildcard.key;

root /var/www/eventschedule/public;
index index.php;

location / {
try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
}

Apache Example

apache.conf
<VirtualHost *:443>
ServerName yourdomain.com
ServerAlias *.yourdomain.com

DocumentRoot /var/www/eventschedule/public

SSLEngine on
SSLCertificateFile /path/to/wildcard.crt
SSLCertificateKeyFile /path/to/wildcard.key

<Directory /var/www/eventschedule/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>

Stripe Subscription Setup

To sell paid plans to your customers, configure Stripe subscription billing. The subscription charges are made on your own Stripe account. This is separate from ticket payments, which are charged on each schedule owner's connected account with no platform fee.

See the Stripe integration documentation for step-by-step key, webhook and Connect instructions.

Required Environment Variables

.env
# Stripe Platform (for subscription billing)
STRIPE_PLATFORM_KEY=pk_live_your_publishable_key
STRIPE_PLATFORM_SECRET=sk_live_your_secret_key
STRIPE_PLATFORM_WEBHOOK_SECRET=whsec_your_webhook_secret
STRIPE_PRICE_MONTHLY=price_monthly_price_id
STRIPE_PRICE_YEARLY=price_yearly_price_id

Those five cover the Pro tier. Selling Enterprise, and showing the right numbers in the interface, needs four more:

Variable Default Description
STRIPE_ENTERPRISE_PRICE_MONTHLY - Stripe Price ID for monthly Enterprise. The "Upgrade to Enterprise" button is hidden until both Enterprise Price IDs are set.
STRIPE_ENTERPRISE_PRICE_YEARLY - Stripe Price ID for yearly Enterprise
STRIPE_PRICE_MONTHLY_AMOUNT
STRIPE_PRICE_YEARLY_AMOUNT
9 / 90 Display-only Pro amounts shown on the subscribe page, the Plan tab and upgrade prompts. A super-admin can change them at /admin/settings, which overrides these for everything the site displays
STRIPE_ENTERPRISE_PRICE_MONTHLY_AMOUNT
STRIPE_ENTERPRISE_PRICE_YEARLY_AMOUNT
29 / 290 Display-only Enterprise amounts, overridable at /admin/settings in the same way
PLATFORM_CURRENCY USD The currency those amounts are shown in, everywhere the platform quotes its own price. Also the fallback currency for a new event whose schedule has no country. A super-admin can change it at /admin/settings, which overrides this value
The amounts are labels, not prices

The *_AMOUNT and PLATFORM_CURRENCY variables only decide what the interface prints. What a customer is actually charged comes from the Stripe Price the matching Price ID points at, including its currency. Set them all, and keep them in step, or your platform will advertise one figure and bill another.

Keep the *_AMOUNT variables set even once you are editing the numbers from /admin/settings. Revenue reporting and renewal emails read these, not the admin panel, so that an amount changed to run a promotion cannot restate revenue you have already booked or quote an existing subscriber a figure their card will never be charged.

Webhook Endpoint

Subscriptions are kept in sync by a webhook that is separate from the ticket-payment one. In your Stripe dashboard add an endpoint pointing at https://app.yourdomain.com/stripe/subscription-webhook and copy its signing secret into STRIPE_PLATFORM_WEBHOOK_SECRET. It is this webhook that downgrades a schedule to Free when its subscription is deleted, and that raises the payment-failed notice, so without it a cancellation in Stripe never reaches your platform.

Do not leave the signing secret blank

Signature checking is only switched on when STRIPE_PLATFORM_WEBHOOK_SECRET has a value. Leave it empty and the endpoint stays open, accepting unsigned requests that could downgrade or upgrade any schedule on your platform. Set it as soon as you create the endpoint.

How Subscriptions Work

  1. A customer creates a schedule. It starts on the Free plan
  2. They open the schedule's admin portal and go to the Plan tab, which shows the current plan, status and the ticket, newsletter and photo allowances
  3. They click Upgrade to Pro and pay. The button only appears once STRIPE_PLATFORM_KEY is set
  4. Pro features unlock for that schedule, and the free-tier footer strip and ad slot come off its public pages
  5. An active Pro subscriber can then switch to Enterprise, or between monthly and yearly, from the same tab. Manage Subscription opens the Stripe billing portal
  6. Subscriptions are per schedule, not per user: a customer with three schedules pays for each one they upgrade

Complete Example Configuration

Here's a complete .env configuration for a SaaS deployment:

.env
# Application
APP_NAME=My Events Platform
APP_ENV=production
APP_DEBUG=false
APP_URL=https://app.myevents.com
APP_MARKETING_URL=https://myevents.com

# SaaS Mode
IS_HOSTED=true

# Branding
APP_LOGO_DARK=/images/dark_logo.png
APP_LOGO_LIGHT=/images/light_logo.png
SUPPORT_EMAIL=support@myevents.com

# Trial Configuration
TRIAL_DAYS=7

# Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=eventschedule
DB_USERNAME=your_db_user
DB_PASSWORD=your_db_password

# Session (important for subdomains)
SESSION_DRIVER=database
SESSION_DOMAIN=.myevents.com

# Mail
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=your_mail_user
MAIL_PASSWORD=your_mail_password
MAIL_FROM_ADDRESS=hello@myevents.com
MAIL_FROM_NAME="${APP_NAME}"

# Stripe Platform (optional, for Pro subscriptions)
STRIPE_PLATFORM_KEY=pk_live_...
STRIPE_PLATFORM_SECRET=sk_live_...
STRIPE_PLATFORM_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_MONTHLY=price_...
STRIPE_PRICE_YEARLY=price_...
Important

Set SESSION_DOMAIN to .yourdomain.com (with leading dot) to allow session sharing across subdomains. If left unset, hosted mode automatically defaults it to your APP_URL base domain; setting it explicitly takes precedence.

Requests arriving on a customer's own domain are the exception: the session domain is cleared for those requests only, so the cookie is scoped to that origin instead of one the browser would reject. That is also why signing in always happens on your app subdomain rather than on a custom domain.

Verification Steps

After completing the configuration, verify your setup:

1. Test the App Subdomain

Visit https://app.yourdomain.com. You should reach the sign-in page, and be able to register an account.

The bare root domain redirects to that same sign-in page. That is the expected result: your platform does not serve the Event Schedule marketing pages, so put your own site on the root domain (or on a separate host) and point APP_MARKETING_URL at it.

2. Test Subdomain Routing

  1. Create a new account and schedule
  2. Note the schedule's subdomain (e.g. my-schedule)
  3. Visit https://my-schedule.yourdomain.com
  4. The schedule's public page should load, and stay signed in when you move back to app.yourdomain.com

3. Test SSL Certificate

Verify SSL works for both:

  • Main domain: https://yourdomain.com
  • Any subdomain: https://test.yourdomain.com

4. Test Subscription Flow (if configured)

  1. Open a schedule's admin portal and select the Plan tab
  2. Click Upgrade to Pro. If the button is missing, STRIPE_PLATFORM_KEY is not set
  3. Complete checkout with the test card 4242 4242 4242 4242, which only works while your keys are the sk_test_ / pk_test_ pair
  4. Confirm the Plan tab now reports Pro, and that the free-tier footer strip has gone from the schedule's public page
  5. Cancel from the Stripe dashboard and confirm the Plan tab picks it up, which proves the subscription webhook is wired correctly

Demo Mode (Optional)

Demo mode lets potential customers try your platform without signing up. Visitors to demo.yourdomain.com are automatically logged in to a demo account with sample data.

How It Works

  • A request to the demo subdomain signs the visitor in as the demo user, with no password prompt
  • They land in the admin portal for the demo schedule, on its Schedule tab, so what they try is the real product rather than a public page
  • The demo interface follows the visitor's browser language, chosen from your supported languages on first visit
  • A visitor already signed in as a real user is bounced back to your app rather than switched into the demo
  • Demo data can be reset periodically to stay fresh

Setting Up Demo Mode

Run the setup command to create the demo account and sample data:

bash
php artisan app:setup-demo

This creates the demo user and a curator schedule on the simpsons subdomain, then populates a small Springfield-themed network around it: talent and venue schedules, sub-schedules, events with ticket types, followed schedules, sample ticket purchases and analytics history.

Two things to check before you run it
  • The demo account is created with the fixed address contact@eventschedule.com. If that address already belongs to a real account on your platform, that account becomes the demo account
  • The demo schedule is created on the Free plan like any other, so Pro-only screens stay locked and its public pages carry your free-tier footer. Grant it a plan from /admin → Schedules if you want to show off paid features

Resetting Demo Data

Running the setup command again will automatically reset the demo data:

bash
php artisan app:setup-demo

Scheduling Automatic Resets (Optional)

To keep demo data fresh, you can schedule hourly resets by adding this to your cron:

crontab
0 * * * * cd /path/to/eventschedule && php artisan app:setup-demo >> /dev/null 2>&1
Note

Demo mode only works in hosted mode (IS_HOSTED=true) since it relies on subdomain routing. The setup command refuses to run otherwise, and the auto-login middleware stays inert, so there is nothing to undo on a selfhosted install.

Troubleshooting

Common Issues

Subdomains show 404 or wrong page

  • Check that IS_HOSTED=true is set
  • Verify wildcard DNS is configured correctly
  • Ensure web server is configured for wildcard subdomains

"Session domain mismatch" or login issues across subdomains

  • Set SESSION_DOMAIN=.yourdomain.com (with leading dot). If unset, hosted mode defaults it to the APP_URL base domain
  • Make sure APP_URL is set to your app subdomain (e.g. https://app.yourdomain.com)
  • Clear browser cookies and try again

Redirect loop, or every visitor logged with the same IP address

  • Set TRUSTED_PROXIES. Left unset, your platform trusts no proxies and reads HTTPS requests as HTTP (see Running Behind a Reverse Proxy)
  • Re-run php artisan config:cache if you have cached your configuration

The root domain shows the sign-in page instead of a landing page

  • This is expected. Marketing pages are only served when IS_NEXUS=true, which is not a setting for your platform
  • Host your own marketing site and point APP_MARKETING_URL at it

SSL certificate errors on subdomains

  • Verify wildcard certificate covers *.yourdomain.com
  • Check certificate is properly installed in web server

Logo not displaying

  • Verify logo files exist in public/images/
  • Check file permissions are readable
  • Ensure paths in .env match actual file locations

Logs

Check the application logs for errors:

bash
tail -f storage/logs/laravel.log

Support Chat

Event Schedule includes a built-in chat system that lets your customers message you for support without leaving the admin portal. It needs no configuration and is present on every hosted install; on a selfhosted install neither the widget nor the admin screen exists. Each customer has one running conversation with you, which reopens if they write again after you have closed it.

For Your Customers

  • Chat widget: A floating chat bubble in the bottom corner of the screen for signed-in users
  • Availability indicator: A green dot on the bubble while you are marked available
  • Sidebar button: A chat icon next to the Help link in the admin sidebar opens the same panel, with a red badge for unread replies
  • Message limit: Up to 2,000 characters per message, in both directions. Any HTML is stripped before the message is stored

For You, the Platform Admin

  • Admin panel: Manage conversations from System → Support in the admin panel at /admin
  • Availability toggle: Switch yourself online to show the green dot. It lapses on its own after four hours, so you never leave it on overnight by accident
  • Conversations list: Every conversation, with unread badges, and a matching badge on the System menu
  • Replying: Open a conversation to read the history and reply
  • Closing conversations: Close resolved conversations to keep the list short

Who Gets Notified

  • Every customer message emails you, whether or not you are marked available, and sends a push notification if OneSignal is configured
  • Your reply emails the customer only when they are not currently in the chat, so an active back-and-forth does not fill their inbox
  • Notifications go to the first account flagged as a platform admin, so keep one dedicated admin account with a monitored address

Custom translations

Rename built-in UI terms to match your customers' vocabulary (for example "Talent" to "Artist", or "Curator" to "Event Planner") without your changes being wiped out by php artisan app:update. Overrides apply globally across every tenant on your platform.

The Easy Way: The Translation Manager

Sign in as a platform admin and open System → Translations in the admin panel. Search for a phrase, edit it for the locale you want, and save. The database is the source of truth: each save is stored as an override and republished to a file on disk, so nothing is lost on the next upgrade. Reverting an override restores the bundled string.

The Manual Way: Override Files

You can also drop a PHP file in:

storage/app/lang/{locale}/{file}.php

The three files you can override are messages.php (UI strings), accessibility.php, and marketing.php. List the keys you want to change and nothing else; the bundled translations fill in the rest:

<?php
// storage/app/lang/en/messages.php
return [
'talent' => 'Artist',
'talents' => 'Artists',
'curator' => 'Event Planner',
'curators' => 'Event Planners',
];

Create one directory per locale you want to override (en, es, fr, …). The full list of supported locales lives in config/app.php under supported_languages.

A hand-written file for one of those three managed groups is adopted into the database the next time the overrides are republished, after which the file is regenerated from the database. Keep that in mind if you edit both by hand and through the admin panel, and keep nested array values in their own group file (validation.php, auth.php or a custom group), which the loader honours and never rewrites.

Rebuilding and Moving Servers

The files are server-local derived state, so rebuild them from the database after restoring a backup or cloning the app to a new machine:

php artisan translations:publish

Run it on each web server, and restart your queue workers afterwards so long-running processes pick up the new strings. If you run several servers behind a load balancer, set LANG_OVERRIDES_PATH to a shared volume instead and publish once. A relative value resolves from the application root; an absolute one is used as given.

Why this works

Changes apply on the next request, with no cache clear required. storage/app/ is gitignored, so your overrides survive php artisan app:update, git pull, and fresh checkouts.

Security Considerations

  1. Environment File: Never expose .env file publicly, and keep APP_DEBUG=false so stack traces never reach a customer
  2. HTTPS Required: Always use HTTPS in production, and keep SESSION_SECURE_COOKIE=true so the shared subdomain cookie is never sent in the clear
  3. API Keys: Keep all API keys and secrets secure
  4. Database: Use strong database passwords and restrict access
  5. File Permissions: Ensure proper file permissions on the server
  6. Admin Accounts: The admin panel at /admin reaches every tenant's data. Flag as few accounts as possible as platform admins, and protect them with two-factor authentication
  7. Proxy Trust: Only widen TRUSTED_PROXIES to * when the origin server cannot be reached except through your proxy. Otherwise list the proxy IPs, so a visitor cannot spoof their own address