Overview
Nova provides an interactive environment where users can:- Learn prompt engineering through structured problems
- Practice against problem test cases (some public, some hidden)
- Compete in timed, attempt-limited challenges
- Submit prompts for automated, criteria-based evaluation
- Track progress through sessions and leaderboards (individual and team)
Nova is a standalone satellite app that lives at
apps/nova, not inside
apps/web. It runs on port 7805 locally (nova.tuturuuu via Portless) and
delegates authentication to apps/web through the shared cross-app login
handoff, the same satellite pattern used by
mail, learn,
teach, mind,
hive, and tasks.Session integrity
Creating a Nova participant submission requires the caller’s ownIN_PROGRESS
session and an associated problem in that session’s challenge. The server rejects
submissions at or after min(start_time + duration, close_at), including before
the expiry cleanup runs. Only an enabled manager of the problem’s own challenge
(resolved server-side from the problem) may submit without a session; managing
a different challenge, or a disabled role, grants no bypass. A supplied session
must satisfy the same ownership, challenge and expiry checks.
After creation, a submission’s problem_id and session_id are immutable for
every caller. Participants cannot edit, delete or grade their own submissions.
Only an enabled manager of the submission’s challenge may write or clear its
grading (/api/v1/submissions/{id}/criteria and /test-cases), and only on a
submission they own may that manager edit the prompt or delete it. A graded criterion must
belong to the submission’s challenge, and a graded test case must belong to its
problem. The grading tables live in private with no authenticated grants,
so the service-role routes are the only write path.
The admin submission detail page applies the same scope: an enabled manager of
the submission’s challenge, or the owner when the submission is bound to their
own session in that challenge. Challenge password verifier fields are never
sent to the client.
Server actions apply their own authorization before using an admin client;
rendering their caller under a protected layout is not sufficient. Whitelist
creation requires an enabled global role manager. Submission detail reads require
the bound session owner or an enabled manager of that challenge; only that
manager may include hidden test cases. Aggregated results require the caller’s
own challenge session, or an enabled challenge manager when selecting another
user or reading a challenge without a session. Disabled manager roles grant no
bypass. These rules cover the retained Next actions and the native Start action
suppliers, without changing database grants or central sign-in.
Regression coverage: src/start-bridge/action-security.test.ts. Native payload
validation is covered by src/start-bridge/action-contracts.test.ts; emitted
transport and browser acceptance remain separate gates.
Active-session reads select the newest start. Session inserts serialize on the
challenge row and reject an overlapping active session with HTTP 409. Expired rows
remain as attempt history and do not prevent a new attempt. Daily attempt limits
use UTC in both the API and database. Criteria retain catalog/password eligibility
before open_at; starting a session still requires the opening window.
Regression coverage: submissions/route.test.ts,
submissions/[submissionId]/route.test.ts,
submissions/[submissionId]/grading-access.test.ts, the admin
submissions/[submissionId]/page.test.tsx,
packages/utils/src/nova/submissions/check-permission.test.ts,
challenge-catalog-access.test.ts, sessions/route.test.ts, and
supabase/tests/private-schema-nova-session-limits.sql. The grant boundary is
covered by private-schema-nova-runtime.sql and
private-schema-nova-challenge-catalog.sql.
Architecture / Satellite Auth
Nova is a Next.js App Router app underapps/nova that owns the prompt
engineering UI and its own /api/v1/* route handlers. Authentication is
delegated to apps/web, and Nova-specific data lives in the shared Supabase
project under the private schema, accessed with the service-role admin client.
Cross-app login handoff
apps/nova/src/proxy.tsruns a centralized auth proxy created withcreateCentralizedAuthProxy({ targetApp: 'nova', sessionMode: 'supabase-first', mfa: { enabled: false } }). MFA is disabled in the satellite becauseapps/webalready enforcesaal2before issuing a cross-app token.- For browser navigation, the proxy first calls
consumeVerifyTokenRequest, which handles the/verify-tokenhandoff so a local Portless login can set cookies onnova.tuturuuu.localhost. It then runs the auth proxy and locale handling. POST /api/auth/verify-app-tokenis generated bycreatePOST('nova', { verificationBaseUrl: TTR_URL })from@tuturuuu/auth/cross-app/server. It exchanges a one-time cross-app token fromapps/webfor Nova’s local app-session.- Token refresh stays same-origin through
POST /api/auth/refresh-app-session; the proxy refreshes the app session for/api/*requests viarefreshAppSessionForRequestand clears Supabase auth cookies on failure.
App-session helpers
apps/nova/src/lib/app-session.ts is the single source of truth for resolving
the current Nova user and their platform role:
getNovaAppSessionUserFromRequest(request)resolves the app-session user from aRequestinside a route handler (targetApp: 'nova').getNovaAppSessionUserFromHeaders()/requireNovaAppSessionUser()resolve the user in Server Components and redirect to/loginwhen absent.getNovaPlatformRole(userId, sbAdmin?)reads the caller’s row from the sharedplatform_user_rolestable (enabled,allow_challenge_management,allow_manage_all_challenges,allow_role_management).requireNovaEnabledRole(user)redirects to/not-whitelistedwhen the role is not enabled.
Database Schema
All Nova tables live in theprivate schema of the shared Supabase project and
are reachable only through the service-role admin client
(createAdminClient({ noCookie: true })). The shapes below are derived from the
generated types in packages/types/src/supabase.ts — treat that file as the
source of truth and never hand-edit generated DB types.
Challenges and problems
A challenge is the top-level unit. Problems belong to a challenge (nova_problems.challenge_id), and test cases belong to a problem
(nova_problem_test_cases.problem_id). Note the relationship direction:
problems reference their parent challenge, not the other way around.
nova_challenges
@tuturuuu/utils/crypto;
new hashes embed their salt, so password_salt is null for them. Any other
non-empty stored hash, including legacy salted SHA-256, is a reset marker: it
never verifies, verify-password returns HTTP 409 PASSWORD_RESET_REQUIRED
without a grant, and a challenge manager must set a new password. Scrypt hashes
with older parameters still verify and are rehashed with an update guarded by
the previous hash. See
shared-secret password reset.
The optional Durable Object password store remains default-off. Once assigned,
each verified user and each trusted source network may spend at most 5% of the
password family’s daily command share, across challenges. Denied probes and
successful-login refunds consume that allowance; rotating challenges or source
addresses cannot reset the user’s quota. See
durable command fairness.
Real-store concurrency and restart coverage lives in
apps/abuse-guard/src/fairness.test.ts.
After the password verifies, /api/auth/challenges/verify-password sets an
httpOnly nova_challenge_access_<challengeId> cookie holding a signed grant
bound to that challenge and the signed-in user, valid for the challenge
duration (at most seven days). Challenge pages accept only that grant; the
stored hash is never sent to the browser, and the retired token cookie is
cleared and no longer grants access. Users with an open challenge from before
this change re-enter the password once. Each password attempt reserves capacity atomically before verification.
Verified success refunds exactly that target reservation. Budgets allow 10
outstanding or failed attempts per challenge and user, plus 60 admitted checks per
user across challenges in 15 minutes. The aggregate budget uses the authenticated
user id, so an office does not share one IP budget. A spent budget answers 429 with
Retry-After (see
shared-secret password budgets).
Starting a session (POST /api/v1/sessions) applies the same participant
catalog rules as the problem APIs and, for a password-protected challenge,
requires that grant for the caller; challenge managers are exempt. Because the
problem APIs only serve participants with an in-progress session, this is what
keeps protected problems behind the password. Sessions and submissions by id
are scoped to their owner, and a session can only be ended, not moved to
another challenge, restarted or reopened. Coverage:
apps/nova/src/app/api/auth/challenges/verify-password/route.test.ts,
apps/nova/src/app/api/v1/sessions/route.test.ts,
apps/nova/src/app/api/v1/sessions/[sessionId]/route.test.ts and
apps/nova/src/app/api/v1/submissions/[submissionId]/route.test.ts.
nova_problems
nova_problem_test_cases
nova_challenge_criteria
Free-form scoring criteria attached to a challenge. Submissions are graded
against these criteria.
Sessions and submissions
nova_sessions
A timed attempt at a challenge.
nova_submissions
A submitted prompt for a problem. The aggregate score is not stored on this
row; per-criterion and per-test-case results live in child tables (see below)
and are aggregated by the nova_submissions_with_scores view.
nova_submission_criteria
Per-criterion grading results for a submission.
nova_submission_test_cases
Per-test-case results for a submission, including whether the model output
matched and the grader’s confidence/reasoning.
Teams
Nova supports team-based participation through three tables (not a single “team management” table):Access control tables
Per-challenge whitelisting and manager assignment are keyed by email:nova_* roles
table. They come from the shared public.platform_user_roles table (enabled,
allow_challenge_management, allow_manage_all_challenges,
allow_role_management), exposed to Nova through
getNovaPlatformRole(). The is_nova_role_manager() RPC reflects the
allow_role_management flag.
Data Access Patterns
Nova route handlers follow a consistent shape: resolve the app-session user, authorize against the caller’s platform role and (for management writes) the challenge they are touching, then read or write theprivate schema with the
admin client.
Listing problems for a challenge
Creating a problem (authorized write)
A problem belongs to a challenge, so the caller must be allowed to manage that specific challenge before the service-role insert runs.Supabase client constructors from
@tuturuuu/supabase/next/server differ:
createClient() and createDynamicClient() are async (await them), while
createAdminClient() is the sync admin client. Nova’s { noCookie: true }
admin entrypoint is awaited as shown above.Authorization Helpers
apps/nova/src/lib/challenge-management-auth.ts centralizes management checks so
service-role writes are never gated by app-session presence alone:
canManageNovaChallenge(user, challengeId, sbAdmin?)— true when the caller can manage all challenges, or hasallow_challenge_managementand is listed innova_challenge_manager_emailsfor that challenge.canManageNovaChallengesGlobally(user, sbAdmin?)— true when the caller can manage all challenges (allow_manage_all_challengesorallow_role_management).canManageNovaRolesGlobally(user, sbAdmin?)— true when the caller hasallow_role_management.- Synchronous predicates
canManageNovaChallenges,canManageAllNovaChallenges, andcanManageNovaRolesevaluate an already-fetchedNovaPlatformRole. - Resolver helpers
getNovaProblemChallengeId,getNovaTestCaseChallengeId, andgetNovaCriterionChallengeIdwalk child rows back to their owning challenge so a singlecanManageNovaChallengecheck can guard nested mutations.
Best Practices
✅ DO
-
Authorize service-role writes explicitly.
getNovaAppSessionUserFromRequest()only proves a valid Nova app-session. Any route that mutates Nova management data throughcreateAdminClient({ noCookie: true })must also check the relevantplatform_user_rolespermission before the write, using the shared helpers inapps/nova/src/lib/challenge-management-auth.ts(canManageNovaChallenge(),canManageNovaChallengesGlobally(),canManageNovaRolesGlobally()). For nested resources (problems, test cases, criteria), resolve the owningchallenge_idfirst via the resolver helpers and then authorize against that challenge. -
Keep hidden test cases hidden. Never return
nova_problem_test_casesrows wherehidden = trueto participants. Filter on the server with.eq('hidden', false)for participant-facing reads. -
Validate challenge timing and attempt limits. Respect
open_at,close_at,previewable_at,max_attempts, andmax_daily_attemptsbefore accepting a submission or starting a session. -
Honor whitelisting. When
whitelisted_onlyis set, only admit emails present innova_challenge_whitelisted_emails. -
Read scores from the aggregate view. Use
nova_submissions_with_scoresinstead of recomputing fromnova_submission_criteria/nova_submission_test_casesin application code.
❌ DON’T
-
Don’t add a Nova tRPC router or
@/lib/novaserver-action layer. Nova has neither; product data flows through the Nova/api/v1/*routes andpackages/internal-apihelpers. -
Don’t read Nova tables directly from the client. All
private.nova_*access goes through service-role route handlers after authorization. -
Don’t invent score columns on
nova_submissions. Scores are derived from the per-criterion and per-test-case child tables.
Related Documentation
- TanStack + Rust migration - the broader move away from
apps/web - Mail satellite app - the canonical satellite auth pattern
- Authentication - cross-app login and sessions
- Database overview - shared schema conventions
Future Enhancements
- Richer team challenges - deeper collaborative prompt engineering flows
- Live leaderboards - real-time ranking surfaced from the leaderboard views
- Prompt history - version control for participant submissions
- Advanced metrics - token usage and latency analysis per submission
- Custom evaluation - configurable, criterion-weighted scoring functions
Management page scope
The global sessions overview requires an enabled global challenge manager. Session and problem details require management access to their owning challenge; assigned managers retain access to their own challenge. Manager membership changes require an enabled platform role. The page and route boundaries are covered byapps/nova/src/test/management-page-access.test.tsx and the challenge
manager route tests.