Skip to main content

Open source event calendar Open source. Every claim here has a file path.

resources/views/marketing/open-source.blade.php lines 1,117 to 1,125 read from this server
<h1 class="hp-h1">
<x-marketing.hero-eyebrow class="es-fade-up es-d-1 hp-eyebrow">Open source event calendar</x-marketing.hero-eyebrow>
<span class="es-mask">
<span class="es-mask-line">Open source. Every claim here</span>
</span>
<span class="es-mask es-mask-2">
<span class="es-mask-line">has a <span class="hp-ink-grad">file path.</span></span>
</span>
</h1>

Event Schedule is open source under the Attribution Assurance License. Selfhost the whole thing on your own server, or drive the hosted version through the REST API. Either way, the code you are trusting is code you can read.

The files this page quotes

01 / The switch

Selfhost, and the plan tiers disappear.

This is not a marketing promise, it is two early returns in one model. isPro() and isEnterprise() both return true the moment the app is not running hosted, so a selfhosted install resolves to the top tier and nothing on it sits behind a paywall.

.env

IS_HOSTED= false

Selling paid tickets

Hosted Pro $5 a month

Yours Included

Free registration and RSVP

Hosted Free Unlimited

Yours Included

Check-in dashboard, waitlists, promo codes and passes

Hosted Pro $5 a month

Yours Included

REST API and webhooks

Hosted Pro

Yours Included

Custom domain

Hosted Enterprise

Yours The install is your domain

Team members on one schedule

Hosted Enterprise Up to five

Yours No cap

Newsletter sends

Hosted 10, 100 or 1,000 emails a month, each recipient counting as one

Yours No monthly cap

The "Powered by" credit

Hosted Pro Removed

Yours Gone, but one small licence credit stays on public pages

Daily import from a list of event URLs

Hosted Not available

Yours Selfhost only

AI parsing and translation

Hosted Our key, with a daily cap per plan

Yours Your own Gemini or OpenAI key, no daily cap

The line that goes the other way

The server, the database, the backups, the TLS certificate, the cron entry and the upgrade window are ours to run.yours to run.

On eventschedule.com the bill for that is $5 a month, or $15 for the two Enterprise lines above. Selfhosting is not free, it is differently priced, and pretending otherwise would be the first false claim on this page.

What selfhosting actually involves

app/Models/Role.php lines 4,005 to 4,028 read from this server
public function isPro()
{
if (! config('app.hosted')) {
return true;
}
 
// Check if user has an active Stripe subscription
if ($this->hasActiveSubscription()) {
return true;
}
 
// Check if user is on a generic trial
if ($this->onGenericTrial()) {
return true;
}
 
// Enterprise plans get all Pro features
if ($this->isEnterprise()) {
return true;
}
 
// Legacy: Check the plan_expires field
return $this->plan_expires >= now()->format('Y-m-d') && $this->plan_type == 'pro';
}

Not hosted: the method answers yes on line 4,008, and never reaches a plan. Hosted: the schedule is asked for a subscription, a trial or a plan. isEnterprise() opens the same way.

02 / Check our work

Nine claims. Nine files.

Nothing on this page is behind a support ticket. Each claim is quoted from the file that proves it, read off the server that sent you this page, and when the proof leaves the file, our build fails.

app/Http/Middleware/ApiAuthentication.php lines 116 to 128 read from this server
$keyPrefix = substr(hash('sha256', $apiKey), 0, 8);
$candidates = User::where('api_key', $keyPrefix)
->whereNotNull('api_key_hash')
->get();
 
$user = null;
foreach ($candidates as $candidate) {
// Verify full API key against bcrypt hash
if (Hash::check($apiKey, $candidate->api_key_hash)) {
$user = $candidate;
break;
}
}

The column holds eight characters of a SHA-256 of the key, enough to find the row. The key itself is checked against a bcrypt hash, so the database never holds a value that works.

Generate a key in your account settings and send it in the X-API-Key header. A key expires a year after it is made.

Pro on eventschedule.com Included on your own server API reference

app/Jobs/SendWebhook.php lines 87 to 89 read from this server
$jsonBody = json_encode($this->payload);
$signature = hash_hmac('sha256', $jsonBody, $this->webhook->secret);
$timestamp = now()->toIso8601String();

An HMAC-SHA256 over the exact body that is sent, so you can verify a delivery came from your install and not from somebody who guessed your endpoint.

Fourteen event types, from sale.created through ticket.scanned to feedback.submitted. Each delivery carries its signature in a header.

Up to three attempts, about thirty then sixty seconds apart, on a real queue; the default sync queue sends once. The secret is stored encrypted.

Pro on eventschedule.com Included on your own server Webhook reference

app/Utils/UrlUtils.php lines 1,191 to 1,199 read from this server
public static function isBlockedIp(string $ip): bool
{
// ---- IPv4 ----
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
// Covers 10/8, 172.16/12, 192.168/16, 127/8, 169.254/16 (incl. the
// 169.254.169.254 metadata address), 0/8, 240/4, 255.255.255.255.
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
return true;
}

Private, loopback and link-local ranges, the cloud metadata address among them, are refused before a request leaves.

An address somebody types in (a webhook target, a calendar feed, a page to import an event from) is resolved first, and refused if it points inside your own network.

Inline scripts carry a nonce, and user markdown is purified before it renders.

On every plan

app/Services/BackupService.php lines 322 to 331 read from this server
'role' => $roleData,
'groups' => $groupsData,
'events' => $eventsData,
'newsletters' => $newslettersData,
'newsletter_segments' => $segmentsData,
'newsletter_ab_tests' => $abTestsData,
'newsletter_unsubscribes' => $unsubscribesData,
'role_subscribers' => $subscribersData,
'gift_cards' => $this->exportGiftCards($role),
'appointment_types' => $this->exportAppointmentTypes($role),

Ticket types, promo codes and sales travel inside each event.

Backup and restore exports a schedule with its events, sub-schedules, ticket types, sales and appointment types, optionally with the images, and imports the same archive into another install.

It is on every plan. That is the honest test of no lock-in, and it is not behind a paywall.

On every plan

config/app.php lines 287 to 300 read from this server
'supported_languages' => [
'ar' => 'arabic',
'de' => 'german',
'en' => 'english',
'es' => 'spanish',
'et' => 'estonian',
'fr' => 'french',
'he' => 'hebrew',
'it' => 'italian',
'nl' => 'dutch',
'pt' => 'portuguese',
'ro' => 'romanian',
'ru' => 'russian',
],

Arabic, German, English, Spanish, Estonian, French, Hebrew, Italian, Dutch, Portuguese, Romanian and Russian, with right-to-left handled properly.

The language list is one array in the config, so adding a thirteenth is a translation job, not a code change.

On every plan

.env.example lines 320 to 325 read from this server
# OneSignal push notifications (optional). When set, the app and visitors'
# browsers communicate with OneSignal's servers and load OneSignal's SDK from
# their CDN. Leave blank to disable push entirely (no SDK is loaded and no
# external calls are made). Get these from the OneSignal dashboard.
ONESIGNAL_APP_ID=
ONESIGNAL_REST_API_KEY=

Blank means off: no script is loaded and no call is made for it.

Front-end libraries are vendored into the repository rather than pulled from a CDN, and every font, the typefaces a schedule can pick for its own page included, is served from your install. Each third-party script the app can load waits for a variable or an account you set (analytics, ads, web push, Stripe's checkout, Cloudflare's bot check and error reporting among them), so a fresh install sends a visitor's browser nowhere else.

SMTP, Stripe or PayPal, Google or Microsoft calendar credentials, an AI key: each is yours to set, or to leave unset, and the feature that needs it stays out of the way until you do.

The server itself is not silent, and it is in the open: once a day it asks GitHub whether there is a newer release, and once a month it downloads a GeoIP file. Both are entries in routes/console.php.

On every plan

app/Services/FederationService.php lines 61 to 64 read from this server
public function isEnabled(): bool
{
return ! config('app.is_nexus') && (bool) Setting::get('federation_enabled');
}

A setting on the instance, not a plan tier, and false until an administrator changes it.

A selfhosted install can share its public events with the eventschedule.com listings, and every listing links back to the event on your own site. A schedule is only listed once someone who manages it chooses to list it.

eventschedule.com is the receiving end and runs a moderation queue instead.

On every plan

config/self-update.php line 27 read from this server
'version_installed' => env('SELF_UPDATER_VERSION_INSTALLED', 'v1.0.139'),

The release this copy reports. On your server it moves when you say so.

A selfhosted install can pull the next release from the admin area, or you can ignore the button and deploy from the tag yourself. Nobody moves your version for you.

On every plan

app/Services/AuditService.php lines 391 to 402 read from this server
return AuditLog::create([
'user_id' => $userId,
'action' => $action,
'model_type' => $modelType,
'model_id' => $modelId,
'old_values' => $oldValues ? self::filterSensitive($oldValues) : null,
'new_values' => $newValues ? self::filterSensitive($newValues) : null,
'ip_address' => request()->ip() ?? '0.0.0.0',
'user_agent' => substr((string) request()->userAgent(), 0, 512),
'metadata' => $metadata,
'created_at' => now(),
]);

What changed and what it was before, with passwords and keys struck out first.

Every schedule keeps a searchable log of what changed, filterable by date and category. A commit log for your calendar, in other words, and it is not a paid add-on.

On every plan
03 / The licence

Permissive, with one condition: keep the credit.

The Attribution Assurance License is OSI-approved and adapted from the BSD licence. It is 31 lines and 361 words, which is the best argument for reading it instead of a summary. So here it is.

LICENSE 31 lines, 361 words

The two conditions that ask something of you What you are given

Copyright (c) 2025 by

Event Schedule * https://www.eventschedule.com

ATTRIBUTION ASSURANCE LICENSE (adapted from the original BSD license)

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the conditions below are met.

These conditions require a modest attribution to Event Schedule (the “Author”), who hopes that its promotional value may help justify the thousands of dollars in otherwise billable time invested in writing this and other freely available, open-source software.

1. Redistributions of source code, in whole or part and with or without modification (the “Code”), must prominently display this GPG-signed text in verifiable form.

2. Redistributions of the Code in binary form must be accompanied by this GPG-signed text in any documentation and, each time the resulting executable program or a program dependent thereon is launched, a prominent display (e.g., splash screen or banner text) of the Author’s attribution information, which includes:

(a) Name ("Hillel Coren"),

(b) Professional identification ("Event Schedule"), and

(c) URL ("https://www.eventschedule.com").

3. Neither the name nor any trademark of the Author may be used to endorse or promote products derived from this software without specific prior written permission.

4. Users are entirely responsible, to the exclusion of the Author and any other persons, for compliance with

(1) regulations set by owners or administrators of employed equipment,

(2) licensing terms of any other software, and

(3) local regulations regarding use, including those regarding import, export, and use of encryption software.

THIS FREE SOFTWARE IS PROVIDED BY THE AUTHOR “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR ANY CONTRIBUTOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, EFFECTS OF UNAUTHORIZED OR MALICIOUS NETWORK ACCESS; PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

The grant

Use it and change it

Redistribution and use in source and binary forms, with or without modification, are permitted. Commercially too. Fork it, strip out what you do not need, run it for a client.

Conditions 1 and 2

Carry the notice

Redistributed source has to display the licence text. A binary redistribution carries it in the documentation and shows the author name, "Event Schedule" and the project URL when the program launches.

What it is not

Not MIT, not AGPL

Permissive rather than copyleft, so your changes are yours to keep private. Also not MIT, which asks for no attribution display. If that distinction matters to a legal team, hand them the file.

The package manifest, composer.json, declares it as AAL, and the licence is listed at opensource.org. None of this is legal advice, and none of it is a substitute for reading the file.

04 / The API

Thirty-eight endpoints, one header.

Send an X-API-Key header, get JSON back: schedules, sub-schedules, events, categories, sales and refunds, appointment booking, boost requests and their monthly statements, and read access to attendee feedback. Here is the whole surface, transcribed from the route file rather than described.

Thirty-eight authenticated endpoints, eight resources

Schedules

5
GET /api/schedules

Every schedule the key can reach

GET /api/schedules/{subdomain}

One schedule, by subdomain

POST /api/schedules

Create a schedule

PUT /api/schedules/{subdomain}

Update it

DELETE /api/schedules/{subdomain}

Delete it

Sub-schedules

4
GET /api/schedules/{subdomain}/groups

List the sub-schedules

POST /api/schedules/{subdomain}/groups

Create one

PUT /api/schedules/{subdomain}/groups/{group_id}

Rename or recolour it

DELETE /api/schedules/{subdomain}/groups/{group_id}

Delete it

Events

8
GET /api/events

List events, paginated

GET /api/events/{id}

One event

POST /api/events/{subdomain}

Create an event on a schedule, or update it by your own id

PUT /api/events/{id}

Update it

DELETE /api/events/{id}

Delete it

POST /api/events/{id}/cancel

Call it off and keep its sales

POST /api/events/{id}/restore

Undo a cancellation

POST /api/events/flyer/{event_id}

Attach a flyer image

Three routes sit outside the list because they are how you get a key in the first place, so they take none themselves: send a verification code, register, log in. routes/api.php

Sales

5
GET /api/sales

List ticket sales

GET /api/sales/{id}

One sale

POST /api/sales

Record a sale

PUT /api/sales/{id}

Mark it paid, cancel it or refund it

DELETE /api/sales/{id}

Delete it

Feedback

2
GET /api/feedback

Post-event ratings and comments

GET /api/fan-content

Fan photos, video and comments

Categories

2
GET /api/categories

The system category list

GET /api/categories/{subdomain}

The effective list for one schedule

Appointments

3
GET /api/schedules/{subdomain}/appointment-types

The types a guest can book

GET /api/appointment-types/{id}/slots

Open times, each with a sentence to read out

POST /api/appointment-types/{id}/bookings

Book one, with the nearest times if it was taken

Boosts

9
GET /api/boosts

A network's boost requests, changed since a time

GET /api/boosts/{id}

One request: the exact ad, its approval and payment

POST /api/boosts/{id}/approve

Approve the version you reviewed

POST /api/boosts/{id}/decline

Send it back with a note

POST /api/boosts/{id}/confirm-payment

Record your own payment reference

DELETE /api/boosts/{id}/confirm-payment

Withdraw it before launch

POST /api/boosts/{id}/stop

Pause it at Meta and end it

GET /api/boost-statements

The monthly fee statements

GET /api/boost-statements/{period}

One month, line by line

Pro Included On eventschedule.com the API is a Pro feature at $5 a month, and the check runs on reads too: a single-resource route answers 403, a list route filters non-Pro schedules out. On a selfhost the same check passes by default. API reference

300
Reads a minute

Per account. Everything that is not a write.

30
Writes a minute

Per account. Creating an event has its own throttle of thirty a minute.

10
Wrong tries

With the same key value, and that value is blocked for fifteen minutes.

app/Http/Middleware/ApiAuthentication.php lines 96 to 97 read from this server
$isWriteOperation = in_array($request->method(), ['POST', 'PUT', 'DELETE']);
$rateLimit = $isWriteOperation ? 30 : 300;

Thirty and three hundred, on the line that sets them.

05 / Ways in

Three ways in.

It is Laravel 11 on PHP 8.2, with MySQL for storage, Vue for the front end and Vite for the build. Nothing exotic to stand up. Pick by how much of the stack you want to touch.

01One click

Softaculous

If your host runs cPanel with Softaculous, Event Schedule is in the installer library. Database, files and permissions are handled for you.

Open the listing
02Compose

Docker

Images and a Compose file live in their own repository, so the application repo stays free of deployment plumbing. The first build takes a few minutes while dependencies install and assets compile. On Alpine images the web user is the numeric UID 82, not www-data.

git clone https://github.com/eventschedule/dockerfiles cd dockerfiles docker compose up --build -d # then open http://localhost:8080
eventschedule/dockerfiles
03By hand

Five steps

Database, files, permissions, environment, cron. The guide names the PHP extensions, the ownership commands and the things that usually go wrong first.

Installation guide
What any of the three needs

PHP 8.2 or newer · MySQL 5.7+ or MariaDB 10.3+ · Apache or Nginx · HTTPS · One cron entry

The PHP version and the extension list are not a recommendation, they are the requirements block of the package manifest. composer.json

And the way out

The proof of no lock-in is the door.

Every vendor says you are not locked in. The test is whether the way out is a feature you can use today, on the free plan, without asking.

  • Take the data app/Services/BackupService.php

    One archive holds a schedule with its events, tickets and sales, and another install reads it back in. See the lines

  • Take the code eventschedule/eventschedule

    What is on GitHub is the product, not a trimmed demo of it. eventschedule.com runs this application, which is why a selfhost gets the features rather than a subset of them.

  • Keep the database

    Underneath all of it is a MySQL database on hardware you chose, which is the part no export format can replace.

Free forever. Upgrade when you're ready.

Free registration for as many people as you like. When you start charging, zero platform fees on every plan - the only deduction is your payment processor's own.

Free
$0 forever, no card

Everything you need to publish a schedule and fill the room, for as long as you like.

  • Unlimited events, sub-schedules and recurring dates
  • Two-way Google, Outlook and CalDAV sync
  • Unlimited free registration and RSVPs, scanned at the door

No expiry, and no card asked for.

Pro Most picked
$5 per month

Start charging, and the rest of the selling kit arrives with it.

  • Sell paid tickets, with the live check-in dashboard
  • Promo codes, add-ons, passes and gift cards
  • The REST API, webhooks, and no Event Schedule branding

7-day free trial, cancel any time.

Enterprise
$15 per month

For a room with a seating chart, a team, and a domain of its own.

  • Reserved seating, drawn once and reused every date
  • Your own domain, and up to five team members
  • Internal and unlisted events, and 1,000 newsletter recipients

Included free on every selfhosted install.

Before you clone it

Frequently asked questions

What developers ask before they clone it.

What licence is Event Schedule under?

The Attribution Assurance License, an OSI-approved licence adapted from the BSD licence. composer.json declares it as AAL and the full text is the LICENSE file in the repository root. It is permissive rather than copyleft: use, modify and redistribute in source or binary form, provided the licence text travels with the code, and provided a binary redistribution displays the author name, "Event Schedule" and the project URL when the program launches. It is short, and it is printed in full further up this page. Read it rather than taking a paragraph on a marketing page for it.

Do I get every feature if I selfhost?

All but one. Role::isPro() and Role::isEnterprise() both return true the moment config('app.hosted') is false, so uncapped ticket sales, the REST API, webhooks, custom fields, unlimited team members and uncapped newsletter sends are simply on. The exception is a custom domain per schedule: ResolveCustomDomain only runs in hosted mode, and on a selfhost the whole install already sits on a domain you chose. Two things you supply yourself: an AI key if you want the parsing and translation features, and a Stripe or PayPal account for payouts.

Is the REST API free on eventschedule.com?

No. API access is a Pro feature at $5 a month, and the check runs on reads as well as writes: under app/Http/Controllers/Api a single-resource route answers 403 for a free schedule, and a list route filters non-Pro schedules out with the wherePro() scope. On a selfhosted install it is on by default, because a selfhost resolves to the top tier.

What are the API rate limits?

Three hundred reads a minute and thirty writes a minute for each account, counted in ApiAuthentication; a write is a POST, PUT or DELETE and everything else is a read. Requests with no key or a wrong one are counted per IP, thirty a minute. Ten failed attempts with the same key value block that key for fifteen minutes, and a key expires a year after it is made. Creating an event has its own throttle of thirty a minute.

How do I install it on my own server?

Three ways. Softaculous does a one-click install on a cPanel host, the eventschedule/dockerfiles repository has images and a Compose file, or you install by hand: PHP 8.2 or newer, MySQL or MariaDB, Apache or Nginx with rewrites, HTTPS, and one cron entry running the scheduler every minute. The installation guide walks through all of it.

Can I take my data out again?

Yes, on every plan. Backup and restore exports a schedule with its events, sub-schedules, ticket types, sales and appointment types, optionally with its images, and imports the same archive into another install. That is the honest test of no lock-in, and it is not behind a paywall.

Do you accept contributions?

Issues and pull requests are open on GitHub. Read the code first: it is a fairly ordinary Laravel 11 application with Vue on the front end, and the parts most people want to change - views, translations, integrations - are where you would expect to find them.

Read it. Then run it.

Clone it and run it on your own hardware, or take a subdomain here and skip the server entirely. Publishing a calendar is free forever either way.

.eventschedule.com
Start for free

No credit card, and no account needed to look.

git clone https://github.com/eventschedule/eventschedule.git

Clone it, read it, open an issue