The archive
02 / CA· Building·92% complete · Shipping August 2026

Castora

Multi-tenant audition management platform. Async video submissions, live online and in-person scheduling, collaborative scoring, ATS, live audition rooms and reusable criteria management.

Added23 Apr 2026Updated31 May 2026getcastora.com
40+Tables
MultiTenant
RTScoring

Overview

Multi-tenant audition management platform. Async video submissions, live online and in-person scheduling, collaborative scoring, ATS, live audition rooms and reusable criteria management.

Screenshots

3 images

Build log

36 entries

  1. Castora's database mechanicals tightened: policies rescoped to authenticated, UUID defaults moved to gen_random_uuid()

  2. Castora self-hosts Mona Sans and drops the Google Fonts dependency

  3. Reconciled every applied DB-audit migration back into version control

  4. Performer pages now live under a clean /my/ namespace

  5. The performer dashboard, redesigned: zones, priority signals, skeletons and empty-state suppression

  6. Rebuilt resource access on a proper RLS junction, with a member-policy gate

  7. Fixed chip slugs colliding with UUIDs on the forms list

  8. Polished the cross-session upload resume after a round of dogfooding

  9. Uploads now resume across sessions — close the tab mid-upload and pick up where you left off

  10. Stalled uploads now abort cleanly and reclaim their orphaned storage keys

  11. Performers can read back their own pre-tape recordings via a scoped signed URL

  12. Every multipart upload is now bound to its owner (key ↔ uploadId ↔ owner)

  13. Closed an intra-tenant hole where an upload key could be written anywhere — keys are now derived server-side

  14. Large audition files upload in resumable multipart chunks

  15. A query that asks every migration if it actually ran

    After getting burned three separate times by migrations that were committed and pushed but never actually applied to the live database, I stopped trusting git as proof that a schema change happened. So I built a standing reconciliation sweep: a single read-only query with one canary check per migration file — does this table exist, this column, this function, this trigger, this enum value, this policy — including expected-to-be-absent checks for everything that was supposed to be dropped. It returns only anomalies, so an empty result means every migration landed exactly where it should. The first run came back completely clean, which was either reassuring or a little anticlimactic. I added a second tier for the trickier cases — the CREATE OR REPLACE and data-seed migrations where mere existence proves nothing, so those get definition and behavioural checks instead. Naturally the first version had a bug where the sort clause referenced an output alias the inner query had not yet exposed, which felt like a fitting way to be reminded that the tool catching my mistakes can make its own.

  16. Committed, pushed, and never actually run

    Chasing down a stubborn coverage warning on the notifications admin page, I found an email template that existed in the codebase, was referenced all over the place, and even had a migration file sitting in the repo, but had simply never been applied to the production database. It turned out to be the third time a migration had been committed but never run, so I also built a read-only reconciliation sweep that checks one canary object per migration against the live database and flags anything that drifted. Lesson firmly relearned: committing a migration and running it are not the same thing, and now there is a query that proves it.

  17. We were reverse-engineering the plan from its features

    The billing page was working out which plan a company was on by looking at which features were switched on: if custom branding was enabled it must be the top tier, and so on. That quietly falls apart the moment you grant a single feature as a one-off override, because a basic-plan company with one bonus flag would suddenly display as a top-tier company. I fixed it at the source so the limits lookup now returns the actual canonical tier name, keeping the feature-flag guessing only as a fallback during the deploy window.

  18. What time is my audition, really?

    Live auditions get scheduled in the company timezone, but the performer might be three timezones away, so a slot reading “10:15” is genuinely ambiguous. I added a small chip that shows both the schedule local time and the viewer local time side by side, and it hides the second half when the two zones match so you never get the silly “10:15 BST, 10:15 BST your time.” Wired it onto the dashboard and the upcoming-slots widget. Small thing, but timezone confusion is precisely the friction that makes someone miss a slot.

  19. An endpoint that trusted whoever called it

    Found a real one. The route that saves reviewer assignments was not checking who was calling it, so any logged-in user could have reshaped which reviewers were on a submission, and worse, the caller identity was being read from the request body rather than the session. I locked it behind a proper company-admin membership check, derived the actor on the server instead of trusting the client, and added an audit-log entry so assignment changes now leave a trail. We are pre-production so no harm was done, but this is exactly the kind of thing you want to catch before real users arrive.

  20. Turning fake buttons into real ones

    Spent a session on accessibility debt that had been quietly accumulating. A bunch of clickable slot rows were just divs with a click handler, which looks fine but gives you no keyboard support and no focus ring, so I converted them to actual buttons and got all of that for free. Added proper ARIA to the onboarding progress bar and the locked audition-mode tiles, reordered the team-invite buttons so the destructive one is not the default, and pulled the redundant back arrows off the detail pages. The same sweep brought a mobile filter drawer to the admin tables so filtering at phone width stops being a cramped mess.

  21. Buttons that promised things they could not deliver

    Did a pass hunting for places where the UI dangled an action you could not actually take: a “Create audition” button showing to people without permission, an Impersonate button on already-deactivated companies, a 404 that dumped you nowhere instead of routing you somewhere useful. Added a small helper so a not-found state sends logged-in users back to their dashboard and everyone else to the public listings. The one that bugged me most was the schedule and analytics pages showing a silent, infinite skeleton when a fetch failed, quietly masking errors as “still loading” rather than telling you something broke.

  22. Stop creating a form just by visiting a page

    For ages, hitting “new form” immediately inserted a draft row into the database, so the moment you clicked away you had left a half-built ghost form sitting in your list. I flipped it so the new-form page just renders a name-and-description input and only writes the row when you actually submit. While I was in there I finally wired up form duplication: it clones the template and all its fields, but deliberately skips the cover image and the submissions, since those belong to the original. Also killed a couple of stat cards that looked clickable but went nowhere.

  23. Forms can be duplicated from the list, and a handful of dead-ends are fixed

    Duplicating an application form is now a one-click action from the list-page dropdown, with an audit entry on the copy so it’s traceable. A handful of UX dead-ends got cleaned up at the same time: the new form page now renders an inline create form instead of silently mounting a draft on load, the Unreviewed stat card no longer navigates to a broken filter state, and the wizard now redirects to the detail page on publish rather than dropping you back at the list. The Today filter on the submissions overview was also wired up — it was visually there but not actually filtering anything.

  24. Auth errors are friendlier and the sign-in flow handles edge cases properly

    Login, registration, password reset, and magic link confirmation all now route through a shared error formatter that produces readable messages for the cases that actually happen in production — wrong credentials, rate limiting, accounts marked for deletion. The reset-password success state now correctly detects whether you already have a live session before redirecting, which previously produced a confusing dead-end. Sign-out also now clears local state before calling the auth provider, so a failed network call can’t leave you in a half-signed-out limbo where the UI thinks you’re logged out but the session is still live.

  25. Tightened soft-delete filtering and rate limiting across public-facing endpoints

    A methodical sweep through the API found several places where soft-deleted records could still be accessed if you knew the right identifier. A shared active-record helper now centralises that filter and is enforced in server-side fetch-by-UUID paths across auditions, forms, pipelines, criteria, and resources — there’s a documented rule in CLAUDE.md so new routes don’t miss it. Rate limits were added to the public-facing endpoints that see the most unauthenticated traffic, and file access was hardened against path traversal and missing deletion-state checks. Nothing dramatic from the outside, but the kind of pass that’s worth doing before volume picks up.

  26. No more skeleton flash when you make changes on the audition page

    The audition detail page used to re-fetch noisily after mutations — trigger an action and you’d briefly see a loading skeleton before the data settled back in. This pass extracted silent-refresh variants of the main data fetchers and rewired every realtime event handler to use them, so the UI updates in place rather than blanking out. The rule is now enforced by a CI guard that counts unguarded loading triggers and fails the build if the number goes up — so it can’t quietly regress as new features land on top of it.

  27. Every irreversible action now asks you to explain why

    Deleting an audition, revoking a membership, withdrawing a submission — these all now go through a confirmation dialog that requires a written reason before the action proceeds. It’s a pattern that existed for some admin-only flows already; this audit pass extended it consistently across the whole platform. Role changes got the same treatment, since a demotion is effectively irreversible if the person later needs to be re-invited. The reason text feeds directly into the audit log, so there’s always a paper trail for anything destructive. The consistent dialog component also means the UX is predictable: you always know what you’re about to do and you always have to confirm it in words.

  28. Performers can join a live waitlist and request reschedules across timezones

    Two big workflow additions landed together: a live waitlist that fills cancellation slots automatically using a FIFO queue with priority holds to prevent race conditions, and a performer-initiated reschedule request that goes through a proper handshake with the company before any slot changes hands. Cross-timezone display is handled throughout, so a performer in Tokyo and a company in London both see times in their own locale. Confirmed slots also now generate a .ics calendar file — it’s a small touch but it means a confirmed audition can go straight into your calendar without copying anything by hand.

  29. Audition outcomes can now be shared directly with performers

    After an audition slot wraps, companies can now record an outcome — notes, scores, a decision — and choose to share it with the performer through the platform. Performers get notified when feedback lands, can view it in their own dashboard, and the no-show rate feeds into a rolling count on their profile. The outcome panel hooks into the existing cast-publish flow from the application stage, which avoided a lot of duplication. Getting the share toggle to trigger the right notification path — rather than just updating the record silently — was the piece that took the most care to get right.

  30. Recording management for live audition sessions

    The live audition room now supports recording capture and storage — companies can attach recordings to individual slots, control visibility, and share with performers when they’re ready. A state machine handles the recording lifecycle cleanly across slot statuses. Also shipped alongside this: a proper vitest unit-test infrastructure and CI enforcement, so the test tier is a first-class part of the build rather than something that runs locally and gets forgotten. The CI gate fails the PR if unit coverage drops, which changes the development rhythm in a good way.

  31. Companies can require pre-recorded material before a live audition

    Some auditions need a self-tape review before inviting performers to an in-person or live session. Companies can now gate slot access behind a pre-tape requirement — performers upload their material, the company reviews it, and only approved submissions unlock booking. The upload sheet handles format validation including a Safari quirk where .mov files arrive without a MIME type. The gating logic lives in a single utility so the rules stay consistent across both the performer-facing and company-facing sides, rather than being duplicated and drifting apart.

  32. Performers can now request and confirm audition slots

    The full slot request lifecycle landed this week — performers browse available slots, send a request, and wait for the company to propose or confirm. Each state transition fires the right notification and clears the right UI state, including the booking handshake on both sides: performers see their confirmation pending, companies see incoming requests and can approve or decline. Getting the cascade right when a slot gets cancelled mid-handshake took a few passes, but it holds up. Venue details were wired in at the same time — the address and location notes stay hidden until a slot is confirmed, so performers only see specifics once they actually have a booking.

  33. Broke prerender by flipping the nitro preset

    Changed nitro.preset thinking it’d fix a routing edge case. It did — and it also silently disabled the static sitemap output. Reverted. Read the module changelog before flipping presets next time. Re-opened the original routing ticket; the fix sits elsewhere.

  34. Tried generating rubrics from PDF briefs

    Directors want to drop a PDF brief and have Castora extract the criteria straight into the scoring rubric. Spiked with pdf-parse plus some heuristics — got 60% of the way on clean templates, falls apart on scanned PDFs. Parked until there’s actual demand.

  35. Fixed the ATS list dropping fresh submissions

    The ATS list was paginating off a cursor that went stale the moment a new submission arrived mid-session. Moved to server-sent events for the row state and killed the cursor. Nicer side effect: the list no longer flickers on each new entry.

  36. Live audition rooms are in

    Wired the WebRTC room infra to the shared scoring panel this afternoon. Three adjudicators, one candidate, scores syncing in real-time without anyone having to refresh. First end-to-end test went clean. Left the old async submission path intact as the fallback — not ripping it out until the live flow has a week of real use behind it.