New
Apps 1,025Makers 1,023This week 1,025
Promote your app−30%
Callboard screenshot 1
1views in
the last 30 days

Callboard

Open-source hackathon project from the AI Engineer "Kill My SaaS" challenge.

Built with Claude Code

by conorbronsdon · from USUnited States · added Oct 29, 2026
no ratings0 comments

Callboard

Archived hackathon project. Callboard was built in August 2026 for the AI Engineer “Kill My SaaS” challenge. It is no longer maintained, and the hosted demo and project site were taken offline in October 2026, so the callboardhq.com links below no longer resolve. The code stays MIT-licensed: see Local development to run it yourself.

Open-source speaker and call-for-proposals management for events.

Callboard covers the workflow from public submission through organizer review, speaker onboarding, agenda publishing, communications, and API/integration exports. It was built for the AI Engineer “Kill My SaaS” challenge as a fast, self-hostable alternative to Sessionboard.

A callboard is the physical backstage board where cast calls, schedules, and notices are posted—the original speaker-management system.

Try the demo

The hosted demo and project site are offline (October 2026). During judging they ran at demo.callboardhq.com and callboardhq.com; the steps below describe that demo and work against a local or self-hosted copy.

Judging this? SUBMISSION_EVIDENCE.md maps every rubric area to the exact URLs that serve it, with one thing to try on each.

Dragging a session on the agenda day board of the live demo

That environment is a disposable, seeded demo. It is periodically reset to the deterministic seed, so anything created there is temporary and will be wiped, and it sends no real mail. Never upload private or sensitive information to it.

A self-hosted deployment behaves differently by default: the checked-in configuration is production-profiled and returns 404 from /demo. One-click organizer and speaker sessions exist only on a disposable demo deployment created from wrangler.demo.example.jsonc.

Suggested walkthrough:

  1. Open /demo, choose Enter organizer workspace, and follow its six-stop judge path.
  2. Open Forms to inspect conditional questions, shared character budgets, routing, close dates, and participant rules.
  3. Open Submissions to inspect the seven-state decision pipeline and commit staged accept/decline queues.
  4. Open Agenda and move a session on the day board; then inspect the dedicated conflict view and published schedule.
  5. Return to /demo, choose Enter speaker portal, then update the profile, complete tasks, upload materials, and read event resources.
  6. Visit Integrations, API keys, and /developers for exports and the Sessionboard-compatible API surface.

What is implemented

Area Current capability
Organizer dashboard Producer-facing programme readiness with explicit blockers and next actions across submissions, speakers, tasks, schedule, rooms, and publication, plus a "Set up your event" checklist for initial event configuration
CFP builder Six-step organizer flow, reusable field registry, conditional logic, cross-field character limits, category/track routing, participant bounds, close dates, limits, and cloning
Public submission Five-step mobile-first SSR flow, drafts, email-verified account entry, abstract/video modes, validation, confirmation email, and portal redirect
Submission decisions Seven status views plus a drag-and-drop kanban board (/admin/submissions/board), staged accept/decline queues, bulk commit, filtering, drill-in, and manual additions
Review operations Standalone assigned-only reviewer workspace, organizer “view as reviewer,” multi-round weighted scoring, reviewer aggregates and completion progress, reviewer provisioning by email with an additive capability flag, team management, round/rubric setup (add/remove criterion rows, numeric, dropdown, or free-text types, lock on first submitted score), per-round blinding, reviewer conflict-of-interest recusal, reviewer reminders, and both batch assignment and direct per-reviewer assignment
Score reporting Aggregate-score sorting in the submissions list, plus a reviewer score CSV export at /admin/submissions/scores.csv carrying one column per round's aggregate, one column per dropdown criterion's answer distribution, one column per free-text criterion's answers, reviewer comments, and the advisory AI-triage opinion kept in its own trailing columns
AI first-pass triage Advisory Workers AI triage on an abstract's detail page: a score, a queue recommendation, and a short rationale carrying the model label. It never writes a review, never changes a submission's status, and degrades to a visible "unavailable in this deployment" when no AI binding is present
Speaker CRM Organization-level contact directory spanning every event, with contact profiles, notes, tags, travel notes, CSV import (preview then commit), duplicate detection and merge, add-to-event, bulk email to a filtered selection, rollups for total contacts/events/returning contacts/top companies, savable named filter segments, and a sourcing pipeline (a kanban board over five fixed stages; each card moves either by dragging or by picking a stage and submitting) with a move-audit trail
Files library Organizer-side browser of every upload for an event, grouped into version chains (newest deliverable first), with per-file comments and a bulk ZIP download
Multi-event Multiple events per deployment, an admin event switcher that scopes every organizer screen, and in-app event creation
Public & embeddable widgets Public event page, schedule with search/day tabs/track-format-room facets and clickable speaker names, session detail, speaker directory/profiles/gallery with consent-gated headshots (monogram fallback when a speaker has not opted in), a unified “Show more” control, browser-local “My schedule” itinerary, and an .ics feed. An organizer embed builder generates four chrome-less widgets (schedule, agenda by day, speakers, speaker gallery) with theme, density, accent, track, custom-CSS, and field-hiding options, emitted as an iframe snippet, a plain link, a calendar feed, or a raw JSON/XML data feed. Saved embeds get a stable ?embed=<id> URL and can be enabled, disabled, or deleted. Full option reference: docs/guides/embed-your-schedule.md
Resource/wiki operations Event-scoped create/edit/order, sanitized preview, publish/unpublish, and recoverable archive-as-unpublish
Speaker portal Status, deadline-gated pending corrections, accepted title/abstract/video corrections with programme-copy sync, profile, headshot/material uploads, tasks, portal forms, resources, and organizer impersonation
Communications Editable templates, confirmation email, task reminders, communication log, and ICS REQUEST/UPDATE/CANCEL lifecycle
Agenda List/day/week/track/room/conflict views, pointer-correct drag and drop, no-JavaScript fallback, room and speaker overlap detection, an “Auto-place remaining” action that fills unscheduled sessions into conflict-free room/time slots, room and track CRUD, organizer session editing with attributed revision history and one-click restore, publishing that holds until speakers have been informed with an explicit per-session override, and public schedule with ordered speaker/co-speaker names
Speaker roster Add and edit speakers, participation status, portal-invite send, CSV import, and portal-form custom fields from organizer assignment through speaker answers
API Scoped keys across six independently grantable scopes (four read, two write — none cascade), consistent envelopes, sessions/speakers/metadata endpoints, OpenAPI document, and developer page
Integrations Byte-exact Accelevents CSV pair, optional API push when configured, non-blocking Airtable mirror, and built-in signed webhooks with optional Svix delivery
Infrastructure One Cloudflare Worker for the main application (D1, R2, React Router framework mode, Drizzle, Tailwind, Vitest, Playwright), plus a separate MCP Worker with no product bindings — see Agents: point your MCP client here

Limitations

Known open gaps, verified against the commit this file ships in. Each one names the file or command that shows it, so it can be rechecked rather than taken on trust.

Volatile counts for the commit this file ships in, one reproducing command each: ls app/db/migrations/*.sql | wc -l → 22 migrations; grep -c 'sqliteTable(' app/db/schema.ts → 41 tables; grep -oE 'insert\("[a-z_]+"' scripts/seed.mjs | sort -u | wc -l → 29 seeded tables; npx vitest list --run | grep -c ' > ' → 2947 tests.

AI triage is advisory only and never decides. Workers AI (the "ai" binding in wrangler.jsonc, model @cf/meta/llama-3.3-70b-instruct-fp8-fast) produces a first-pass score, a queue recommendation, and a short rationale on an abstract's detail page (app/lib/review/ai-triage.server.ts, app/components/ai-triage-card.tsx). It never writes a reviews row, never appears in the reviewer CSV, and never changes a submission's status — the committee decides. A deployment with no AI binding still runs; every triage surface degrades to a visible “unavailable in this deployment.” The agenda's “Auto-place remaining” is a separate deterministic conflict-aware planner (app/lib/agenda/autoplace.ts), not a model.

Submission statuses are a fixed set of seven. SESSION_STATUSES in app/db/schema.ts is a hard-coded list — draft, pending, accept_queue, accepted, decline_queue, declined, withdrawn. Organizers cannot rename, reorder, or add one. Submissions can be worked from filtered list views and a drill-in, or from a drag-and-drop kanban board (/admin/submissions/board, app/routes/admin.submissions.board.tsx) — but the board's columns are exactly those seven fixed statuses, not an organizer-configurable status set.

The CRM sourcing pipeline's stages are fixed. The kanban at /admin/pipeline lays contacts out in five hard-coded stage columns (PIPELINE_STAGES in app/db/schema.ts: prospect, contacted, in_conversation, confirmed, declined). A card can be moved by dragging it between columns or with its stage select and Move button; both submit the same audited move action. There is still no stage-management UI to add, rename, reorder, or remove a stage: the ordering is product workflow, not organizer configuration.

The bulk file ZIP selection is capped at 30 MB of source bytes. The Files library at /admin/files lists every upload for an event grouped into version chains with per-file comments, and /admin/files/download streams a selection as a ZIP. MAX_ZIP_BYTES in app/lib/zip.ts is 30 * 1024 * 1024: the archive is assembled in Worker memory (deliberately not Zip64), so a selection whose declared source bytes total over the cap is refused, with its own size quoted, before any object is fetched. The cap bounds the source selection, not the finished archive: buildZip (same file) adds a local header and a central-directory record per entry plus filenames and an end-of-central-directory record, so a selection sitting right at the cap produces a ZIP slightly larger than 30 MB, not one capped at exactly that size.

Saved segments are deployment-global and name-keyed. A Contacts filter (search, company, title, event, notes, tag) can be saved as a named segment (contact_segments in app/db/schema.ts), but the table has no owner column and a unique index on the name: every organizer on the deployment sees the same segments, there is no private or per-organizer segment, and saving over an existing name overwrites it (save-segment and delete-segment in admin.contacts.tsx are the only actions).

Contact merge is one-way. Merging sets people.merged_into on the losing row and re-parents its memberships, tags, notes, and session participations. There is no unmerge action; the UI says so before you confirm.

Custom-field tasks provisioned by a later acceptance carry no due date. Assigning a portal form writes an absolute due date onto the speakers chosen at the time, but the reusable template it can also write stores dueOffsetDays: null (admin.tasks.tsx), because an absolute date cannot be turned into the relative offset templates hold. commit.server.ts maps that null straight through to a null dueAt, so speakers accepted later get the questions without a deadline until the organizer sets one on the task.

Saved embeds cannot be edited in place. The save action always mints a new crypto.randomUUID(), so changing a widget's theme, track, or accent means saving a second embed and repasting its snippet on the host page. Existing embeds can be enabled, disabled, or deleted, but not amended.

Embed track filters are only rename-safe when saved. Saved embeds store the track id and resolve the name at render, so a saved embed survives a track rename. Two cases do not: a snippet copied straight from a widget card carries ?track=<name> and is matched by name at render (app/lib/embeds.server.ts), and saved rows written before the id-keying change still hold a name and resolve to themselves. Either one silently becomes an empty widget on somebody else's page when the track is renamed. Re-save any embed whose track filter matters after renaming a track.

The public “My schedule” itinerary is browser-local. Starred sessions live in window.localStorage in app/routes/public.schedule.tsx. It needs JavaScript, does not sync across devices or browsers, is not tied to an account, and is lost when site data is cleared. The rest of the public schedule — search, day tabs, track/format/room facets — works without JavaScript.

Review blinding hides structured identity, not prose. A blinded round's session_participants query never selects an identity column — only a count() is projected (app/routes/review.index.tsx:136-145) — so name, email, company, and title never reach the reviewer. The abstract body is shown verbatim, so a submitter who names themselves in their own text is still identifiable, and un-blinding a round does not retroactively hide what a reviewer already saw before you re-checked the box. The claim is checkable in the reviewer query itself (app/routes/review.index.tsx:114-143): the blinded branch never selects the identity columns, so they cannot reach the wire.

The review engine generalizes to awards and grants judging; awards vocabulary does not exist yet. Weighted rubrics, enforced per-round blinding, reviewer recusal, and multi-round assignment are judging primitives, not CFP-specific ones — nothing in app/lib/review/ assumes "submission" over "nominee" or "applicant." Renaming the vocabulary and shipping a public winners page is deliberate post-competition roadmap, not a gap in what ships today.

Reviewer invitations report the delivery state the mailer actually observed, never a presumed send. An outside reviewer can be provisioned by email and gains the capability immediately (invite-reviewer in admin.reviews.tsx), and — on a deployment with real mail configured — the same sign-in email every other magic link uses is genuinely sent. The invite panel reports whichever state the mailer actually returned (sent only when a configured provider accepted the message, logged on the console driver, failed or unknown otherwise), so it never claims a delivery it did not observe. Team membership is still restricted to reviewer-capable people — people.role = "admin", an event role in admin/organizer/reviewer, or the additive event_people.is_reviewer flag — so an arbitrary contact cannot be dropped onto a review team without being provisioned first.

Revision history is read without a bound. Organizer session edits and speaker corrections are versioned in session_revisions and restorable, with attribution, from /admin/sessions/<id> (app/lib/admin/session-revisions.server.ts). The history query orders by recency with no LIMIT, so a session edited a very large number of times reads its entire revision list in a single loader pass.

One D1 database, one region. wrangler.jsonc declares a single DB binding with no read replication configured, so read latency is bounded by the database's primary region regardless of where the Worker runs.

Uploads are Worker-proxied and capped at 25 MB. MAX_UPLOAD_BYTES in app/lib/portal-uploads.ts is 25 * 1024 * 1024. There are no presigned URLs: every byte in and out passes through the Worker so ownership checks stay on both paths, which also makes 25 MB a deliberate ceiling rather than a tunable.

The judge demo sends no real mail. The demo template pins MAIL_DRIVER=console, so confirmation emails, invitations, reminders, and decision notifications are printed to the Worker log instead of delivered. Demo mode reveals the magic link in the sign-in response so a judge can proceed without a mailbox. Real delivery is a production feature, not a gap: the Resend driver ships in this repo and native Gmail calendar invites were verified end-to-end (app/lib/comms/ics.ts:5). The judged demo keeps mail on the console deliberately — an anonymous-writable demo must not be able to email strangers. Want to see live delivery? Ask, and we'll stand up a mail-enabled instance scoped to addresses you control. Outlook ICS lifecycle rendering remains unverified.

Submission evidence, including the requirement-to-screen map, is in SUBMISSION_EVIDENCE.md.

Verify it yourself

Every row is a claim this README makes, paired with the exact command or URL that proves or disproves it. Repository commands run from a clean checkout after npm ci. The same rule holds for every figure in this README, not just this table: each one is either generated from the deployment (a constant named in its source file, a count with the command that produces it) or paired with the command that reproduces it.

Claim How to check it
The migration, table, and seeded-table counts are real Run the three commands stamped at the top of Limitations; each prints the number quoted beside it
The test suite is the size claimed npx vitest list --run | grep -c ' > '. Limitations stamps this count for the commit the file ships in — run it against your checkout and it should match exactly. (Piping to plain wc -l also counts each worker process's one-line Node ExperimentalWarning banner, so raw line count varies run to run — filter to lines matching ' > ', vitest's list-item separator, for a stable number.)
The whole gate passes three consecutive isolated times npm run release:verify:repeat — writes one attempt per pass to artifacts/release-gate/<sha>/manifest.json and only sets "result": "passed" after three
CI runs that same gate, not a lighter one .github/workflows/check.yml and .github/workflows/repeat-release-gate.yml
Calendar invites actually land in a real calendar app/lib/comms/ics.ts:5 records the end-to-end verification against Gmail on 2026-08-08: native RSVP card, event auto-staged, conflict detection ran
AI triage never decides anything `grep -nE '.(inse

Description taken from the repository README.

Ratings

–
0 ratings
5 0
4 0
3 0
2 0
1 0

Rate this app

Sign in to rate

Comments 0

Sign in to comment

No comments yet.

Related apps

#hackathon · #saas · #productivity