Back to Calendar PDCAL Help

Administrator's Manual

How to administer PDCAL

Configuration, roles and permissions, and day-to-day operation — for whoever sets up and runs your district's PD calendar.

What PDCAL is

PDCAL plans a K–12 district's Professional Development calendar across a July–June fiscal year, and tracks substitute-teacher coverage against configured daily allotments so gaps show up before they become a problem on the day.

Events carry a title, date/time, substitute need, facilitators, and tags. The Overview dashboard rolls every month up into subs-available / subs-needed / subs-remaining totals; Month and Week views show the day-by-day detail. Fiscal years can be imported in bulk from a Kickup opportunity export, or entered by hand.

Architecture at a glance

PDCAL runs as a Cloudflare Worker (built on Hono) backed by a Cloudflare D1 database. The browser signs in with Google, exchanges that for PDCAL's own session cookie, and talks to a JSON API for everything else — there's no offline or local-only mode.

Client

Browser

Google Sign-In, then talks to /api/* via a session cookie.

Server

Cloudflare Worker

Checks role & tag scope on every request, serves the API and static files.

Storage

Cloudflare D1

Users, fiscal years, events, tags, allotments, audit log.

The session cookie is HttpOnly, Secure, and SameSite=Strict, valid for 7 days — but every request re-checks that user's role and status in the database, so suspending someone or changing their role takes effect on their very next click, not after a token eventually expires.

Deploying & configuring PDCAL

You'll need Node, a Cloudflare account with wrangler, and a Google Cloud project.

  1. Install dependencies

    npm install
  2. Create a Google OAuth client

    Google Cloud Console → Credentials → Create OAuth client ID → type Web application. Add every origin PDCAL will be served from under Authorized JavaScript origins (e.g. http://localhost:8787 for local dev, plus your production domain). That's the whole Google Cloud setup — no API key, Drive/Picker APIs, or extra scopes are needed.

  3. Set the client ID in both places it's read from

    The same OAuth client ID must match in wrangler.jsonc's vars.GOOGLE_CLIENT_ID (server-side verification) and public/index.html's window.PDCAL_GOOGLE_CONFIG.clientId (the Sign In button) — or sign-in fails with an audience mismatch.

  4. Set the two secrets

    Never commit these. Locally, copy .dev.vars.example to .dev.vars; in production:

    wrangler secret put JWT_SECRET
    wrangler secret put SUPER_ADMIN_EMAILS

    JWT_SECRET signs session cookies — rotating it logs everyone out. SUPER_ADMIN_EMAILS is covered next.

  5. Run migrations, then deploy

    npm run db:migrate:local   # local dev
    npm run db:migrate:remote  # production D1
    npm run dev                # local server
    npm run deploy              # wrangler deploy --minify

Bootstrapping the first Super Administrator

Nobody is pre-seeded in a fresh database — SUPER_ADMIN_EMAILS is the only way in. It's a comma-separated, case-insensitive allowlist checked on every login:

SUPER_ADMIN_EMAILS=you@yourdistrict.org,other-admin@yourdistrict.org

The first time a listed address signs in with Google, PDCAL promotes it to Super Administrator automatically — no one has to grant it by hand, even if that address has never touched PDCAL before.

Everyone else's first sign-in

Anyone else gets a read-only Default session until an admin explicitly grants them a role in Manage Users — they can see the calendar, but nothing else.

Removing an email from the allowlist doesn't demote anyone

Once promoted, editing SUPER_ADMIN_EMAILS has no retroactive effect — the role only changes going forward, through Manage Users. Treat this variable as a one-time bootstrap, not an ongoing membership list.

Roles & permissions

Four roles, strictly ranked. Every role includes everything the roles below it can do.

Role View calendar Create / edit / delete events Manage users Fiscal-year settings¹
Default (Viewer) Every fiscal year — — —
Editor Every fiscal year Within assigned tag scope — —
Administrator Every fiscal year Within assigned tag scope Up to Administrator —
Super Administrator Every fiscal year Unrestricted All users, incl. Super Admins Yes

¹ Substitute allotments & exemptions, tag categories, CSV import/export, Save/Load .pdcal files, and Delete All Events.

Read access is global; edit access is scoped

Every signed-in role — including a scoped Editor, and even Default/Viewer — sees every event on every day. Tag scope never hides anything from view; it only ever governs who may edit or delete a given event.

Scoped access (tag-based)

One tag category per fiscal year can be flagged Primary (see Tag Categories below). Only tags from that category can be assigned to a user as their scope, and only those tags gate edit/delete rights on an event. An Editor or Administrator can edit an event if at least one of the event's Primary-category tags is also in their own assigned scope; a Super Administrator can always edit anything.

Two scope gotchas worth knowing before you assign roles

  • An Editor or Administrator with no tag scope assigned is effectively read-only. Edit rights require an actual overlap between the caller's scope and the event's tags — an empty scope can never overlap anything. Assign at least one Primary-category tag in the same step you grant the role, or the account can view the calendar but can't touch it.
  • An event with no Primary-category tag can only be edited or deleted by a Super Administrator — regardless of who created it. There's no ownership fallback. If your district uses scoped Editors, make sure every event they create picks up a Primary-category tag, or they'll immediately lose the ability to edit their own event.

Guardrails

  • Only a Super Administrator can create, edit, suspend, or delete another Super Administrator's account, or grant the role to anyone.
  • The last remaining Super Administrator account can't be demoted, suspended, or deleted.
  • No one can suspend or delete their own account.
  • An Administrator can only hand out tag scope that's a subset of their own assigned scope — they can't delegate access they don't personally hold.

Managing users

Sidebar → Manage Users, visible to Administrator and Super Administrator accounts.

Adding someone

Enter their email, choose a role, and — for any role except Super Administrator — check one or more scope tags, then Add. This pre-provisions the account before they've ever signed in; it stays reserved until they sign in with that Google address for the first time.

Editing, suspending, deleting

  • Edit — change role or scope tags inline, then Save. This fully replaces that user's tag scope, not adds to it.
  • Suspend / Unsuspend — blocks or restores access immediately, on their very next request.
  • Delete — removes the account entirely.

Why some accounts don't show up in the list

Manage Users hides plain Default/Viewer accounts, except your own row — that always shows, so you can confirm your own role and scope.

Audit log

Every user create/role-change/suspend/unsuspend/delete, and every event delete, is recorded with actor, action, target, and timestamp. There's no page for it yet — Administrators and Super Administrators can pull it directly while signed in:

GET /api/audit-logs
GET /api/audit-logs?actorUserId=12&action=user.role_change&limit=200

Fiscal-year configuration

Everything below lives in the sidebar and requires Super Administrator.

Substitute daily allotment & exemptions

Sidebar → Config. Set how many substitutes the district can deploy on each weekday; add an exemption (a date or range) to suspend the normal allotment over a scheduled break. Every day cell computes:

Subs Available = Daily Allotment for that weekday − Total Subs Needed that day

Save / Load .pdcal files

These are backup and restore tools now, not primary storage — the fiscal year already lives in the database. Save downloads the selected fiscal year as JSON. Load reads a file back in and overwrites that fiscal year's live data for every viewer. Take a fresh Save before you Load if you're not certain.

Danger zone — Delete All Events

Wipes every event in the selected fiscal year. Doesn't touch other fiscal years, tags, or substitute settings — but there's no undo. Save a backup first.

Tag categories & scope

Sidebar → Tag Categories (Super Administrator only). Add categories; under each, add tags with a color — colors live on the tag, not the category.

Exactly one category per fiscal year can be flagged Primary, which:

  • Is the only category whose tags can be assigned to a user as edit scope.
  • Is the only category that gates edit/delete rights on an event.
  • Auto-assigns an event's color tag on CSV import, when a row has exactly one tag in it.

Every other category is purely descriptive — useful for filtering, never for restricting access.

Decide your Primary category before assigning scope

Re-flagging it later effectively redefines who can edit what. Pick the dimension you actually want to gate access by — commonly Department or Building — before granting any scoped role.

Importing & exporting data

Sidebar → Import CSV / Export CSV / CSV Template — all Super Administrator only. Import expects a raw Kickup opportunity export (the "Scratch Sheet" CSV, straight from kickup.co), not a hand-built file. You'll see a create/update/ignore/error preview before anything is written, then a full report afterward.

Day-to-day operation

  1. Sign in with Google from the top-right button.

  2. Pick a fiscal year from the dropdown in the left controls sidebar. Each year's data is independent.

  3. Start from Overview — a tile per month with a subs available/needed/remaining summary. Click a tile to open that month.

  4. Add an event with the + button on any day. Required fields mirror Kickup: Event ID, Event Name, Learning Type, Start/End Date, and Hours, using MM/DD/YYYY HH:MM:SS AM/PM.

  5. Set tags and an event color — pick one selected tag as the Event Color Tag; the card takes its color.

  6. Reschedule by drag, or copy/cut/paste an event card between days.

Editing something outside your scope

A card outside your assigned scope opens read-only with a "View only" note — the server enforces the same rule, so this isn't just a UI hint.

The light/dark toggle at the bottom of the sidebar is a local display preference — it works for every role and isn't part of the shared plan data.

Quick reference

Environment variables

GOOGLE_CLIENT_IDSet in wrangler.jsonc. Not secret. Must match the client ID in index.html.
JWT_SECRETSecret. Signs session cookies.
SUPER_ADMIN_EMAILSSecret. Comma-separated bootstrap allowlist.

Role capability matrix

Capability Default Editor Admin Super Admin
View calendar✓✓✓✓
Create/edit/delete events (in scope)—✓✓✓
Manage Users (up to own rank)——✓✓
Allotments, tags, CSV, Save/Load, Delete All———✓
Manage Super Administrator accounts———✓