Lawyers live in their calendar app, not in our case management system. Instead of building two-way sync with each calendar provider, we give every user a private calendar subscription URL. Outlook, Google Calendar and Apple Calendar can all subscribe to it. This post shows how we built it in Jlsa, a legal case management software for law firms in Saudi Arabia.
Why a subscription feed and not API sync
A feed is one URL that returns an .ics file. The calendar app fetches it on a schedule. That gives us:
- No OAuth app per provider, and no stored access tokens for user mailboxes.
- One code path for every calendar app.
- Read-only by design. Nobody edits a hearing from their phone calendar by mistake.
The cost is delay. The calendar app decides how often to refresh. That is fine for court hearings, which are set days or weeks ahead.
1. A secret token per user
Each user gets a random token. We use a ULID, which is 26 characters:
$user->ical_token = (string) Str::ulid(); The route sits outside the auth middleware, because a calendar app cannot log in. It is rate-limited:
Route::get('ical/{token}', [CalendarController::class, 'ical']) ->middleware('throttle:5,1') ->name('ical-calendar'); 2. Check the token before anything else
$token = preg_replace('/[^0-9A-Za-z]/', '', $token); abort_if(strlen($token) !== 26, 403); $user = User::where('ical_token', $token)->first(); abort_if(! $user || $user->isDisabled() || ! $user->can('access_calendar'), 403); Three rules matter here:
- A disabled user's feed stops at once, even though the URL still exists in their calendar app.
- The feed respects the same permissions as the web app. A lawyer sees only the hearings of cases they can open.
- An admin setting turns the whole feature on or off for the firm. When it is off, the route returns 404.
3. Build the calendar
We use spatie/icalendar-generator:
$calendar = Calendar::create('Hearings') ->productIdentifier('-//RSE//Legal-System') ->refreshInterval(60); foreach ($hearings as $hearing) { $calendar->event( Event::create(strip_tags($hearing->lawsuit->name)) ->uniqueIdentifier((string) $hearing->getKey()) ->startsAt($hearing->appointment_at) ->endsAt($hearing->appointment_end_at ?? $hearing->appointment_at->copy()->addHour()) ->createdAt($hearing->created_at) ->status($hearing->deleted_at ? EventStatus::Cancelled : EventStatus::Confirmed) ->address($hearing->court?->name) ->description(Str::limit(strip_tags($hearing->message), 200)) ); } return response($calendar->get(), 200, [ 'Content-Type' => 'text/calendar; charset=utf-8', 'Content-Disposition' => 'attachment; filename="cal.ics"', ]); Some details are easy to get wrong:
- Stable UID. The event UID is the database key. If it changes, calendar apps show duplicates after each refresh.
- Cancelled, not removed. We query with
withTrashed()and send deleted hearings withSTATUS:CANCELLED. Some clients keep an event that just disappears from the feed. A cancelled status removes it reliably. - A default end time. Many hearings have no end time. Without
DTEND, some clients show a zero-length event. We default to one hour. - Plain text only. Case names and notes can contain HTML from the editor. Strip it and limit its length.
- Time zone. The app runs on
Asia/Riyadh, so times leave the server in the zone the lawyer expects.
4. Show users how to subscribe
The calendar page has a Calendar sync button. It opens a dialog with the feed URL, a copy button and a link to Microsoft's guide for subscribing in Outlook. The user pastes the URL into "Subscribe from web" or "From URL". It is not a file import, which would not update.
What we would add next
- Separate feeds per case type, for firms with large teams.
- Hearing reminders as
VALARMentries, so they reach the phone even when email is missed.
ملخص بالعربية: يشرح هذا المقال كيف يوفر برنامج إدارة القضايا لمكاتب المحاماة رابط اشتراك خاصًا لكل محام. ويعرض الرابط مواعيد الجلسات في تطبيق التقويم لديه. ويتناول المقال حماية الرابط برمز سري، وإيقافه فور تعطيل الحساب، وظهور الجلسات الملغاة في التقويم على أنها ملغاة.
Written by the engineering team at Renewable Systems in Riyadh. Jlsa is our case management system for Saudi law firms and in-house legal departments.


