> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Nova - Prompt Engineering Platform

> Architecture and implementation of the Nova prompt engineering platform

Nova is Tuturuuu's prompt engineering platform designed to help users learn, practice, and compete in AI prompt engineering challenges.

## 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)

<Note>
  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](/platform/applications/mail), [learn](/platform/applications/learn),
  [teach](/platform/applications/teach), [mind](/platform/applications/mind),
  [hive](/platform/applications/hive), and [tasks](/platform/applications/tasks).
</Note>

## Architecture / Satellite Auth

Nova is a Next.js App Router app under `apps/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.ts` runs a centralized auth proxy created with
  `createCentralizedAuthProxy({ targetApp: 'nova', sessionMode: 'supabase-first', mfa: { enabled: false } })`.
  MFA is disabled in the satellite because `apps/web` already enforces `aal2`
  before issuing a cross-app token.
* For browser navigation, the proxy first calls `consumeVerifyTokenRequest`,
  which handles the `/verify-token` handoff so a local Portless login can set
  cookies on `nova.tuturuuu.localhost`. It then runs the auth proxy and locale
  handling.
* `POST /api/auth/verify-app-token` is generated by
  `createPOST('nova', { verificationBaseUrl: TTR_URL })` from
  `@tuturuuu/auth/cross-app/server`. It exchanges a one-time cross-app token from
  `apps/web` for 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 via `refreshAppSessionForRequest` and 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 a `Request` inside a route handler (`targetApp: 'nova'`).
* `getNovaAppSessionUserFromHeaders()` / `requireNovaAppSessionUser()` resolve
  the user in Server Components and redirect to `/login` when absent.
* `getNovaPlatformRole(userId, sbAdmin?)` reads the caller's row from the shared
  `platform_user_roles` table (`enabled`, `allow_challenge_management`,
  `allow_manage_all_challenges`, `allow_role_management`).
* `requireNovaEnabledRole(user)` redirects to `/not-whitelisted` when the role
  is not enabled.

<Warning>
  There is **no Nova tRPC router** and no Nova server-action data layer in
  `apps/web`. Nova route handlers authenticate the caller, then read and write the
  `private.nova_*` tables directly with the service-role admin client. Use
  `packages/internal-api` helpers and the Nova `/api/v1/*` routes for client data
  access; do not add direct client-side Supabase reads of Nova tables.
</Warning>

## Database Schema

All Nova tables live in the `private` 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`

```sql theme={null}
CREATE TABLE private.nova_challenges (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  title text NOT NULL,
  description text NOT NULL,
  duration integer NOT NULL,            -- session duration in seconds
  enabled boolean NOT NULL DEFAULT false,
  max_attempts integer NOT NULL,
  max_daily_attempts integer NOT NULL,
  open_at timestamptz,
  close_at timestamptz,
  previewable_at timestamptz,
  whitelisted_only boolean NOT NULL DEFAULT false,
  password_hash text,
  password_salt text,
  created_at timestamptz NOT NULL DEFAULT now()
);
```

#### `nova_problems`

```sql theme={null}
CREATE TABLE private.nova_problems (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  challenge_id uuid NOT NULL REFERENCES private.nova_challenges(id),
  title text NOT NULL,
  description text NOT NULL,
  example_input text NOT NULL,
  example_output text NOT NULL,
  max_prompt_length integer NOT NULL,   -- caps how long a submitted prompt may be
  created_at timestamptz NOT NULL DEFAULT now()
);
```

#### `nova_problem_test_cases`

```sql theme={null}
CREATE TABLE private.nova_problem_test_cases (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  problem_id uuid NOT NULL REFERENCES private.nova_problems(id),
  input text NOT NULL,
  output text NOT NULL,
  hidden boolean NOT NULL DEFAULT false, -- hidden test cases are not exposed to participants
  created_at timestamptz NOT NULL DEFAULT now()
);
```

#### `nova_challenge_criteria`

Free-form scoring criteria attached to a challenge. Submissions are graded
against these criteria.

```sql theme={null}
CREATE TABLE private.nova_challenge_criteria (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  challenge_id uuid NOT NULL REFERENCES private.nova_challenges(id),
  name text NOT NULL,
  description text NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);
```

### Sessions and submissions

#### `nova_sessions`

A timed attempt at a challenge.

```sql theme={null}
CREATE TABLE private.nova_sessions (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  challenge_id uuid NOT NULL REFERENCES private.nova_challenges(id),
  user_id uuid NOT NULL,
  status text NOT NULL,
  start_time timestamptz NOT NULL,
  end_time timestamptz,
  created_at timestamptz NOT NULL DEFAULT now()
);
```

#### `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.

```sql theme={null}
CREATE TABLE private.nova_submissions (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  problem_id uuid NOT NULL REFERENCES private.nova_problems(id),
  session_id uuid REFERENCES private.nova_sessions(id),
  user_id uuid NOT NULL,
  prompt text NOT NULL,
  overall_assessment text,
  created_at timestamptz NOT NULL DEFAULT now()
);
```

#### `nova_submission_criteria`

Per-criterion grading results for a submission.

```sql theme={null}
CREATE TABLE private.nova_submission_criteria (
  submission_id uuid NOT NULL REFERENCES private.nova_submissions(id),
  criteria_id uuid NOT NULL REFERENCES private.nova_challenge_criteria(id),
  score numeric NOT NULL,
  feedback text NOT NULL,
  strengths text[],
  improvements text[],
  created_at timestamptz NOT NULL DEFAULT now()
);
```

#### `nova_submission_test_cases`

Per-test-case results for a submission, including whether the model output
matched and the grader's confidence/reasoning.

```sql theme={null}
CREATE TABLE private.nova_submission_test_cases (
  submission_id uuid NOT NULL REFERENCES private.nova_submissions(id),
  test_case_id uuid NOT NULL REFERENCES private.nova_problem_test_cases(id),
  output text NOT NULL,
  matched boolean NOT NULL DEFAULT false,
  confidence numeric,
  reasoning text,
  created_at timestamptz NOT NULL DEFAULT now()
);
```

### Teams

Nova supports team-based participation through three tables (not a single
"team management" table):

```sql theme={null}
CREATE TABLE private.nova_teams (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name text NOT NULL,
  description text,
  goals text,
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE private.nova_team_members (
  team_id uuid NOT NULL REFERENCES private.nova_teams(id),
  user_id uuid NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

-- Invite-by-email roster for a team
CREATE TABLE private.nova_team_emails (
  team_id uuid NOT NULL REFERENCES private.nova_teams(id),
  email text NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);
```

### Access control tables

Per-challenge whitelisting and manager assignment are keyed by email:

```sql theme={null}
-- Emails allowed to participate when a challenge is whitelisted_only
CREATE TABLE private.nova_challenge_whitelisted_emails (
  challenge_id uuid NOT NULL REFERENCES private.nova_challenges(id),
  email text NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

-- Emails granted management rights over a specific challenge
CREATE TABLE private.nova_challenge_manager_emails (
  challenge_id uuid NOT NULL REFERENCES private.nova_challenges(id),
  email text NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);
```

Platform-wide Nova permissions are **not** stored in a dedicated `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 the `private` schema with the
admin client.

### Listing problems for a challenge

```typescript theme={null}
// apps/nova/src/app/api/v1/problems/route.ts (simplified)
import { createAdminClient } from '@tuturuuu/supabase/next/server';
import { NextResponse } from 'next/server';
import { getNovaAppSessionUserFromRequest } from '@/lib/app-session';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const challengeId = searchParams.get('challengeId');

  // App-session resolution is synchronous; do NOT await it.
  const user = getNovaAppSessionUserFromRequest(request);
  if (!user?.id) {
    return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
  }

  // createAdminClient is synchronous, but { noCookie: true } returns a promise
  // in this entrypoint, so await it here.
  const sbAdmin = await createAdminClient({ noCookie: true });

  let query = sbAdmin
    .schema('private')
    .from('nova_problems')
    .select('*')
    .order('created_at', { ascending: false });

  if (challengeId) {
    query = query.eq('challenge_id', challengeId);
  }

  const { data: problems, error } = await query;
  if (error) {
    return NextResponse.json(
      { message: 'Error fetching problems' },
      { status: 500 }
    );
  }

  return NextResponse.json(problems, { status: 200 });
}
```

### 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.

```typescript theme={null}
// apps/nova/src/app/api/v1/problems/route.ts (simplified)
import { createAdminClient } from '@tuturuuu/supabase/next/server';
import { NextResponse } from 'next/server';
import { getNovaAppSessionUserFromRequest } from '@/lib/app-session';
import { canManageNovaChallenge } from '@/lib/challenge-management-auth';
import { createProblemSchema } from '../schemas';

export async function POST(request: Request) {
  const user = getNovaAppSessionUserFromRequest(request);
  if (!user?.id) {
    return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
  }

  const validatedData = createProblemSchema.parse(await request.json());
  const sbAdmin = await createAdminClient({ noCookie: true });

  // Authorize against the target challenge before the privileged write.
  if (!(await canManageNovaChallenge(user, validatedData.challengeId, sbAdmin))) {
    return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
  }

  const { data: problem, error } = await sbAdmin
    .schema('private')
    .from('nova_problems')
    .insert({
      title: validatedData.title,
      description: validatedData.description,
      max_prompt_length: validatedData.maxPromptLength,
      example_input: validatedData.exampleInput,
      example_output: validatedData.exampleOutput,
      challenge_id: validatedData.challengeId,
    })
    .select()
    .single();

  if (error) {
    return NextResponse.json(
      { message: 'Error creating problem' },
      { status: 500 }
    );
  }

  return NextResponse.json(problem, { status: 201 });
}
```

<Note>
  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.
</Note>

## 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 has `allow_challenge_management` and is listed in
  `nova_challenge_manager_emails` for that challenge.
* `canManageNovaChallengesGlobally(user, sbAdmin?)` — true when the caller can
  manage all challenges (`allow_manage_all_challenges` or `allow_role_management`).
* `canManageNovaRolesGlobally(user, sbAdmin?)` — true when the caller has
  `allow_role_management`.
* Synchronous predicates `canManageNovaChallenges`, `canManageAllNovaChallenges`,
  and `canManageNovaRoles` evaluate an already-fetched `NovaPlatformRole`.
* Resolver helpers `getNovaProblemChallengeId`, `getNovaTestCaseChallengeId`, and
  `getNovaCriterionChallengeId` walk child rows back to their owning challenge so
  a single `canManageNovaChallenge` check can guard nested mutations.

## Best Practices

### ✅ DO

1. **Authorize service-role writes explicitly.**

   `getNovaAppSessionUserFromRequest()` only proves a valid Nova app-session.
   Any route that mutates Nova management data through
   `createAdminClient({ noCookie: true })` must also check the relevant
   `platform_user_roles` permission before the write, using the shared helpers in
   `apps/nova/src/lib/challenge-management-auth.ts`
   (`canManageNovaChallenge()`, `canManageNovaChallengesGlobally()`,
   `canManageNovaRolesGlobally()`). For nested resources (problems, test cases,
   criteria), resolve the owning `challenge_id` first via the resolver helpers and
   then authorize against that challenge.

2. **Keep hidden test cases hidden.** Never return `nova_problem_test_cases`
   rows where `hidden = true` to participants. Filter on the server with
   `.eq('hidden', false)` for participant-facing reads.

3. **Validate challenge timing and attempt limits.** Respect `open_at`,
   `close_at`, `previewable_at`, `max_attempts`, and `max_daily_attempts` before
   accepting a submission or starting a session.

4. **Honor whitelisting.** When `whitelisted_only` is set, only admit emails
   present in `nova_challenge_whitelisted_emails`.

5. **Read scores from the aggregate view.** Use `nova_submissions_with_scores`
   instead of recomputing from `nova_submission_criteria` /
   `nova_submission_test_cases` in application code.

### ❌ DON'T

1. **Don't add a Nova tRPC router or `@/lib/nova` server-action layer.** Nova
   has neither; product data flows through the Nova `/api/v1/*` routes and
   `packages/internal-api` helpers.

2. **Don't read Nova tables directly from the client.** All `private.nova_*`
   access goes through service-role route handlers after authorization.

3. **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](/platform/architecture/tanstack-rust-migration) - the broader move away from `apps/web`
* [Mail satellite app](/platform/applications/mail) - the canonical satellite auth pattern
* [Authentication](/platform/architecture/authentication) - cross-app login and sessions
* [Database overview](/reference/database/schema-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
