Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.schedkit.net/docs/llms.txt

Use this file to discover all available pages before exploring further.

SchedKit Architecture

Platform Overview

SchedKit is an API-first incident and assignment coordination platform — not a calendar app. The scheduling layer exists to solve a specific ops problem: getting the right person on-scene or on-call as fast as possible, with a full audit trail, SLA tracking, and real-time awareness. The pitch is intentionally tactical:
SchedKit: API-FirstResponder. Signal in. Assignment out. No clicks required.
Every capability in the platform — booking, dispatching, incident tracking, org management — is a first-class API resource. The dashboard is a read/write UI built on the same endpoints your agents and automation scripts call.

Core Concepts

┌─────────────────────────────────────────────────────────────────┐
│                        SchedKit Platform                        │
│                                                                 │
│  SIGNALS ──────→  INCIDENTS/TICKETS ──────→  ASSIGNMENTS       │
│  (input layer)     (triage layer)            (dispatch layer)   │
│                                                                 │
│  Beacons            Status + SLA             Bookings           │
│  Captures           Priority routing         Confirmation flow  │
│  Alerts             SSE broadcast            Calendar sync      │
│  Notes              Push notifications       Webhook delivery   │
│                                                                 │
│                         ORGS / TEAMS                           │
│                    (multi-tenant scope layer)                   │
└─────────────────────────────────────────────────────────────────┘

Signals

Signals are the input layer — raw events from devices, operators, or automated systems.
TypeDescription
beaconLive GPS ping from an active operator (ephemeral — not persisted per ping)
capturePhoto or image attached to a field observation
noteText note with optional coordinates
checkinManual “I’m here” with location
alertHigh-priority signal — triggers push notification to org members
Beacon pings are not stored per-ping — only beacon_on (first activation) and beacon_off (stop) are persisted for audit. Live position is broadcast via SSE only. All signals are org-scoped: only members of the relevant org receive them on the SSE stream.

Beacons

Beacons are the continuous-tracking variant of signals. An active beacon:
  1. Fires a beacon signal on the first ping, writing beacon_on to the log
  2. Broadcasts live position to org SSE subscribers on every subsequent ping (no DB write)
  3. Writes beacon_off when stopped, clearing the operator from the live map
The beacon fast-path was a deliberate design decision: GPS pings in field ops can be 1–5 second intervals. Writing every ping to NocoDB would crush performance.

Incidents / Tickets

Incidents and tickets are the same object in the same database table — just two mental models over one schema.
  • /v1/tickets — for helpdesk/ITSM async flows
  • /v1/incidents — for real-time dispatch + SSE war-room flows
Both operate on identical records. Fields:
  • title, description, status (open → in_progress → resolved → closed)
  • priority (low / normal / high / urgent)
  • source (api / email / webhook / alert)
  • sla_due_at, sla_breached, sla_status — computed SLA window based on priority
  • lat, lng, location_name — incident coordinates
  • customer_token — magic link for a public status page (no login required)
  • assignee_id — responder assigned to this incident
SLA windows: urgent=1h, high=4h, normal=24h, low=48h.

Assignments / Bookings

Bookings are the dispatch output — a scheduled assignment linking an attendee (responder) to a host (coordinator) at a specific time. Booking flow:
  1. GET /v1/slots/:username/:event_slug?date=YYYY-MM-DD — available windows
  2. POST /book/:username/:event_slug — submit booking
  3. Email confirmation sent to attendee; optional host confirmation step
  4. cancel_url and reschedule_url included in all confirmation emails
If requires_confirmation: true on the event type, status is held as pending until the host clicks confirm/decline from their email. Team bookings (/v1/book/:org_slug/:team_slug/:event_slug) auto-assign to an available team member using the team’s routing strategy: round_robin or random.

Orgs / Teams

Multi-tenant scoping works like this:
Org (e.g. "Alpha Response")
  ├── Members (roles: owner / admin / member)
  └── Teams (e.g. "Night Shift")
        ├── Team Members (active/inactive toggle for rotation)
        └── Team Event Types (bookable via /book/:org_slug/:team_slug/:et_slug)
All signals, incidents, and bookings are scoped to an org. SSE streams filter by org membership.

Technology Stack

LayerTechnology
API serverFastify (Node.js 22) — ESM, plugin-based
DatabaseNocoDB — REST meta-API over a SQL base
AuthMagic link + 6-digit code — no passwords, sessions in NocoDB
EmailMailjet — transactional confirmation, invite, and SLA emails
PushWeb Push API (VAPID) via web-push npm package
Real-timeServer-Sent Events (SSE) — org-scoped, Nginx-buffered off
CalendarGoogle Calendar API — optional OAuth per user
PaymentsStripe — coming (plan enforcement hooks already wired)
DocsMintlify + Swagger/OpenAPI (@fastify/swagger)
DeployPlesk GitOps — git push main → webhook → deploy.shpm2 restart

SSE Architecture

Client                     Fastify (GET /v1/signals/stream)
  │                              │
  │──── GET /stream ────────────→│
  │                              │  Auth: session or api_key
  │                              │  Lookup user's org IDs
  │←── 200 text/event-stream ───│
  │←── data: {"type":"connected"}│
  │                              │  Register reply in signalClientsByOrg[orgId]
  │                              │
  │    (other client POSTs a signal)
  │                              │  broadcastSignal(orgId, event)
  │←── data: {"type":"signal.beacon",...} ─────────────────────────│
  │                              │
  │                         25s keepalive ping ": ping"
There is no message broker (Redis, etc.) — SSE state is in-process. This is a single-process Node.js app (pm2 cluster mode disabled). If we ever need horizontal scale, the SSE layer would need a Redis pub/sub adapter.

Key Design Principles

1. API-First

Every feature is a durable HTTP endpoint before it’s a UI element. The dashboard is a consumer of the API, not the other way around. This makes the platform agent-native by default.

2. Org-Scoped Everything

No data leaks between orgs. SSE streams, signal feeds, ticket lists, booking access — all filtered by org membership. There’s no global admin query.

3. Agent-Native

SchedKit was designed for automation from day one:
  • API key auth on every endpoint
  • Machine-readable responses (no HTML-only paths except public booking pages)
  • Webhook delivery on booking create/cancel
  • SSE streams suitable for EventSource consumption from any agent runtime
  • customer_token on incidents for zero-login status polling

4. No Click-Ops Deploys

Infrastructure is GitOps only. git push origin main triggers Plesk webhook → deploy.sh. No SSH-and-restart, no Plesk UI deploy buttons.

5. Fail Gracefully, Log Everything

  • Emails, webhooks, Google Calendar sync, and push notifications are all non-blocking (try/catch, fire-and-forget)
  • Core booking/signal operations return success even if downstream delivery fails
  • Failures are logged via fastify.log.error

Data Flow: Incident Response Example

Sensor / Alert System


POST /v1/tickets
  { title, priority: "urgent", source: "alert", lat, lng }

       ├──→ Write to NocoDB (tickets table)
       ├──→ SSE broadcast: incident.created → all war-room clients
       ├──→ Push notification → user's registered browsers
       └──→ ntfy.sh alert → user's phone

Dispatcher (human or agent)


POST /v1/book/:username/:event_slug
  { start_time, attendee_name, attendee_email }

       ├──→ Slot validation
       ├──→ Write booking
       ├──→ Confirmation email → attendee
       └──→ Webhook POST → event type webhook_url

PATCH /v1/tickets/:id { status: "in_progress", assignee_id }

       └──→ SSE broadcast: incident.updated