A Next.js application with Supabase backend for a 350-item political orientation survey (108 conceptual + 242 applied questions, including 48 deliberate collision scenarios and a 50-item controversy-stress layer). The current bank is v2.2, which appends 50 overt, neutrally worded high-conflict items (abortion, economic systems, extreme wealth, reparations, firearms, gender identity, immigration enforcement, capital punishment, nuclear first use) to the v2.1 comprehension revision. From v2.1: every question pairs a technical proposition with a plain-language "In other words" restatement, 152 scenarios carry an explicit "Assume that" premise, and the response scale separates "Neither / genuinely balanced" (scored 0) from "Not sure / need more information" (recorded as null, excluded from scoring). v2.1 is not assumed psychometrically interchangeable with v2.0; results store their bank version.
- 11 Core Axes: Economic coordination, distribution & property, liberty & public order, territorial authority, cultural continuity, scope of obligation, sovereignty, technology, ecology, moral objectivity, value structure
- 7 Style Facets: Change strategy, institutional confidence, justice style, democratic constraint, epistemic authority, democratic mediation, force & peace
- 38 Political Archetypes: Automatically matched based on your profile
- 48 Collision Scenarios: 24 mirrored axis pairs that price one value against another
- Rich Visualizations: Radar charts, axis scales, flavor bar charts
- Admin Analytics Dashboard: Response trends, population averages, popular types
- Anonymous survey: No login is required; optional AI context is disclosed and consented to separately
- Shareable Results: Unique URL for each completion
- Optional AI-assisted interpretation: Consent-based, cached, evidence-linked analysis of commitments, center scores, tensions, possible hypocrisy, low salience, and knowledge gaps
Local development and CI require Node.js 22.3 or newer. The route-level test suite uses Node's module-mocking test API to exercise the real Next handlers.
- Go to supabase.com and create a new project
- Once created, go to SQL Editor
- Paste the contents of
supabase/fresh_install.sqland run it once — this installs the complete current state (schema, policies, 18 constructs, 350 questions, all links, semantic coverage metadata, and the private AI analysis cache). Do not runschema.sql/seeds/migrations separately for a new project; seedocs/supabase-migration.mdfor details and for migrating data from an old project. - Go to Settings → API and copy:
- Project URL
anonpublic keyservice_rolekey (server-only; required for AI analysis and admin routes)
Or manually:
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Set environment variables
vercel env add NEXT_PUBLIC_SUPABASE_URL
vercel env add NEXT_PUBLIC_SUPABASE_ANON_KEY
vercel env add SUPABASE_SERVICE_ROLE_KEY
# Redeploy with env vars
vercel --prodCreate .env.local for local development:
NEXT_PUBLIC_SUPABASE_URL=https://YOUR_PROJECT.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
# Optional; remains unavailable unless explicitly enabled and configured.
AI_ANALYSIS_ENABLED=false
AI_ANALYSIS_PROVIDER=openai
AI_ANALYSIS_PROMPT_VERSION=v1
OPENAI_API_KEY=
OPENAI_ANALYSIS_MODEL=
ANTHROPIC_API_KEY=
ANTHROPIC_ANALYSIS_MODEL=
AI_ANALYSIS_MAX_REGENERATIONS=3
AI_ANALYSIS_MAX_ATTEMPTS=6
AI_ANALYSIS_CONTEXT_MAX_CHARS=2000
AI_ANALYSIS_TIMEOUT_MS=240000
RUN_LIVE_AI_EVALS=falseSet exactly one selected provider's key/model pair, then change
AI_ANALYSIS_ENABLED to true. Provider keys and the Supabase service role are
server-only and must never use a NEXT_PUBLIC_ prefix. See
docs/ai-analysis.md for architecture, safety,
thresholds, caching, and evaluation.
| Route | Description |
|---|---|
/ |
Landing page with feature overview |
/survey |
350-question questionnaire (108 conceptual + 242 applied), resumable across sittings |
/results/[sessionId] |
Individual results with visualizations |
/results/[sessionId]/analysis |
Optional candid AI-assisted interpretation and refinement |
/admin |
Analytics dashboard |
/admin/questions |
Question management (add/edit/delete) |
# Install dependencies
npm install
# Copy environment template
cp .env.example .env.local
# Edit .env.local with your Supabase credentials
# Run development server
npm run dev
# Open http://localhost:3000polyaxis/
├── src/
│ ├── app/
│ │ ├── page.tsx # Landing page
│ │ ├── survey/page.tsx # Survey questionnaire
│ │ ├── results/[sessionId]/ # Results display
│ │ └── admin/
│ │ ├── page.tsx # Analytics dashboard
│ │ └── questions/page.tsx # Question manager
│ ├── components/
│ │ ├── charts/
│ │ │ ├── CoreAxesRadar.tsx # Spider/radar chart
│ │ │ ├── AxisScale.tsx # Horizontal scale bars
│ │ │ ├── FlavorCharts.tsx # Flavor visualizations
│ │ │ └── AdminCharts.tsx # Analytics charts
│ │ ├── admin/
│ │ │ ├── QuestionEditor.tsx # Add/edit question form
│ │ │ └── QuestionList.tsx # Question list with actions
│ │ └── ResultsActions.tsx # Share/copy buttons
│ └── lib/
│ ├── instrument.ts # Axis definitions & default questions
│ ├── questions.ts # Question CRUD operations
│ ├── scorer.ts # Scoring calculations
│ ├── analytics.ts # Admin data fetching
│ └── supabase.ts # Database client
└── supabase/
├── schema.sql # Database schema
├── seed.sql # Conceptual questions (98 items)
└── seed_applied.sql # Applied questions (52 items)
- Radar Chart: 10-axis spider chart showing overall profile shape
- Axis Scales: Gradient bars with position markers for each dimension
- Flavor Bar Chart: Horizontal bars comparing archetype matches
- Expandable Flavor List: Detailed cards for all matching types
- Line Chart: Daily response volume over 30 days
- Horizontal Bar Chart: Population average by axis (red/green for polarity)
- Flavor Popularity: Most common archetype matches weighted by rank
- Recent Sessions: Quick links to individual results
- Axis Sidebar: View all core axes and facets with question counts
- Question Editor: Add new questions with text and pole direction
- Question List: Edit, reorder, activate/deactivate, or delete questions
- Balance Indicator: Shows if questions are balanced between poles
Questions are stored in Supabase and can be edited without redeploying.
After running schema.sql, seed the default questions:
-- Run in Supabase SQL Editor
-- First, load the 98 conceptual questions
\i seed.sql
-- Or paste contents of supabase/seed.sql
-- Then, load the 52 applied questions
\i seed_applied.sql
-- Or paste contents of supabase/seed_applied.sql| Field | Description |
|---|---|
axis_id |
Which axis (C1-C10, F1-F3) |
key |
-1 = agreement indicates negative pole, +1 = positive pole |
text |
The question statement |
display_order |
Order in the survey |
active |
Whether to include in survey |
question_type |
'conceptual' or 'applied' |
weight |
Scoring multiplier (1.0 conceptual, 1.15 applied, 1.25 collision scenarios) |
- Balance poles: Each axis should have ~equal questions for each pole
- Avoid double-barreled: One concept per question
- Clear wording: Avoid jargon, keep statements simple
- Test changes: Deactivate rather than delete to preserve data
| Column | Type | Description |
|---|---|---|
| id | UUID | Primary key |
| session_id | TEXT | Unique session identifier |
| responses | JSONB | Raw responses {item_id: value} |
| created_at | TIMESTAMPTZ | Submission timestamp |
| Column | Type | Description |
|---|---|---|
| id | UUID | Primary key |
| session_id | TEXT | Links to responses |
| core_axes | JSONB | Array of axis scores |
| facets | JSONB | Array of facet scores |
| top_flavors | JSONB | Top 5 matching archetypes |
| created_at | TIMESTAMPTZ | Calculation timestamp |
Stores versioned provisional/refined structured reports, their deterministic
signals, cache identity, provider provenance, token/latency metadata, and safe
failure status. It references the bank-pinned result by session_id. RLS is
enabled with no browser policies; generation and retrieval are service-role
server operations. Existing v2.2 databases add it with
supabase/migrations/20260717180000_add_result_ai_analysis.sql.
- Archetype descriptions and survey questions were authored specifically for this project by the Polyaxis contributors and are original to the repository. No third-party licensed text was incorporated.
- If future updates incorporate external wording or data, include clear attribution (source name, URL, and license) in this section and ensure the license permits reuse within this project.
- Confirm the text is original or properly licensed for inclusion; record attribution and license details here if external.
- Keep wording concise, neutral, and free of jargon; avoid double-barreled statements.
- Maintain balance across axes (similar counts per pole) and verify
axis_id,key, anddisplay_ordervalues are consistent with schema expectations. - Run through the survey flow locally to ensure new items render correctly and results pages reflect the changes.
- Update seed data or migration scripts if the additions require database changes.
The schema includes views for aggregate analytics:
-- Daily response counts
SELECT * FROM daily_responses;
-- Average scores by axis
SELECT * FROM aggregate_scores;
-- Most popular flavor matches
SELECT * FROM popular_flavors;Custom queries:
-- Distribution of a specific axis
SELECT
CASE
WHEN (axis->>'score')::float < -0.5 THEN 'Strong Left'
WHEN (axis->>'score')::float < 0 THEN 'Lean Left'
WHEN (axis->>'score')::float < 0.5 THEN 'Lean Right'
ELSE 'Strong Right'
END as bucket,
COUNT(*) as count
FROM survey_results,
LATERAL jsonb_array_elements(core_axes) as axis
WHERE axis->>'axis_id' = 'C1'
GROUP BY bucket;
-- Correlation between axes (requires tablefunc extension)
SELECT
a.session_id,
(SELECT (ax->>'score')::float FROM jsonb_array_elements(a.core_axes) ax WHERE ax->>'axis_id' = 'C1') as economic_control,
(SELECT (ax->>'score')::float FROM jsonb_array_elements(a.core_axes) ax WHERE ax->>'axis_id' = 'C3') as coercive_power
FROM survey_results a;Edit src/lib/instrument.ts:
export const ITEMS: Item[] = [
// ... existing items
{
id: 99,
order: 99,
axis: "C1",
key: 1, // 1 = agree pushes score positive, -1 = agree pushes negative
text: "Your new question here."
}
]export const FLAVOR_ARCHETYPES: FlavorArchetype[] = [
// ... existing archetypes
{
id: "new_type",
name: "New Political Type",
description: "Description of this archetype.",
color: "#HEX",
components: [
{ axis: "C1", direction: 1, weight: 1.0 },
{ axis: "F2", direction: -1, weight: 0.5 }
]
}
]The app uses Supabase directly from the client. If you need server-side API routes:
// src/app/api/submit/route.ts
import { NextResponse } from 'next/server'
import { supabase } from '@/lib/supabase'
import { calculateScores } from '@/lib/scorer'
import { nanoid } from 'nanoid'
export async function POST(request: Request) {
const { responses } = await request.json()
const sessionId = nanoid(12)
const results = calculateScores(responses)
await supabase.from('survey_responses').insert({
session_id: sessionId,
responses
})
await supabase.from('survey_results').insert({
session_id: sessionId,
...results
})
return NextResponse.json({ sessionId, results })
}- Bundle Size: ~150KB gzipped (including all question data)
- Time to Interactive: <2s on 3G
- Database: Indexed queries, typically <50ms
- The survey can be completed anonymously; session IDs are random 12-character strings and results are shareable only by direct URL.
- AI analysis is optional, requires explicit consent, and never changes deterministic scores.
- Bank-pinned answers and optional context are sent only to the configured AI provider after consent. Do not enter identifying details in the context field.
- Provider payloads exclude session ID, user ID, email, IP, and auth claims; OpenAI request storage is disabled.
- Provider keys, raw stored context, and the Supabase service role are never exposed to the browser. The AI table has no direct client read/write policy.
- Application logs must not contain full prompts, raw answer maps, provider output, or user context.
MIT