Skip to main content
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)
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.

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

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

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

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

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.
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.
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.
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.
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.
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):
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:
-- 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

// 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.
// 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 });
}
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 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.

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