Serving assessments as iuvius, on their own domain.
How we deliver a fully iuvius-branded candidate assessment on assess.iuvius.com — branding, routing, reports, admin access, and data compliance — without duplicating the backend, and in a way we can repeat for the next client.
~3 weeks · 1 engineerSingle release · UAT-gatedPilot · 14-day exitContract: signed 2026-06-30Candidate app untouched by default
00
Locked decisions
Candidate email
Jonathan sends invites from iuvius.com
Genuine @iuvius.com, zero ESP lead-time. We suppress our platform emails + reminders for the org. Automated SES sending is an optional add if AWS approval + DNS land in-window.
Deployment model
One Coolify service per client
Brand chosen by a runtime BRAND env var. Full isolation from the live Utkrusht app. Evolvable to host-based later with no rewrite.
Legal docs
Iuvius provides privacy + terms URLs
Candidate-facing legal links point at iuvius-hosted pages.
Reports
Branded PDF + full branded report
Downloadable iuvius PDF, AND a full-parity Iuvius-branded report at assess.iuvius.com/report/[token] (Option B). In scope for this build.
Console
Jonathan self-serves with scoped admin
Creates positions, invites candidates, views & shares reports. Data auto-isolated by organization_id. The recruiter console itself stays Utkrusht-branded — it is not candidate-facing, so §2.3 does not touch it.
Release model
Single release — nothing to Jonathan until it's done
No interim console handoff or stopgap link; internal UAT gates the release, and the full branded report ships with everything else. Realistic total ≈ 3 weeks, 1 engineer — the original "4 days" is now Milestone 1.
01
What the contract binds us to
Not just "put their logo on it." Three clause groups drive real scope beyond cosmetics.
Clause
Obligation
Scope
§2.2 / 2.3
Zero Utkrusht identifiers candidate-facing — name, logo, footer, watermark, browser tab title, and email branding.
frontend + email
§2.5
Preferred: candidate flow on assess.iuvius.com (their subdomain), subject to feasibility.
ops-only
§2.6
Results, reports, scores, recordings, code submissions accessible to iuvius.
admin access
§3.1–3.5
Candidate data siloed — never shared with another recruiter; no model-training/marketing without written approval.
org scoping
§3.6
Disclose where data is processed/stored (India / cloud subprocessors).
1-page doc
§3.7 / 3.8
Candidate + iuvius can request export / deletion; delete within 30 days.
The load-bearing insight
The candidate app can be white-labeled with zero backend code. Full contract compliance (§2.3 email + §3 data lifecycle) needs a small, bounded backend change plus a compliance runbook — that's the real, and only, backend work.
02
Architecture & the request flow
We deploy a second instance of the same candidate Next.js app, distinguished by one runtime env var, and route assess.iuvius.com to it. It talks to the same Python backend and the same database — isolation is by organization_id, not separate infrastructure. Only the Next.js frontend app is duplicated; Flask, FastAPI, the notifications service, Supabase, and S3 are single and shared.
Iuvius pathShared / untouchedRouting layer
One image, two deployments. The brand is resolved server-side at request time from BRAND — so a single image serves every client and moving to a host-based single service later is a one-line change.
The flow, end to end
DNS — assess.iuvius.com is a CNAME (Jonathan creates it) → our Coolify host.
Traefik sees Host: assess.iuvius.com → forwards to the Iuvius candidate-app container. TLS auto-issued (Let's Encrypt).
The Iuvius container — identical image, started with BRAND=iuvius. SSR renders with the Iuvius brand: logo, colors, tab title, favicon, legal links, no Crisp.
Data calls go to the shared Python backend (flapi/api.utkrusht.ai). The session belongs to the Iuvius org → everything read/written is scoped to organization_id.
Candidate sees a fully iuvius-branded experience on iuvius's own domain, with the Utkrusht platform working invisibly underneath.
Why runtime BRAND, not NEXT_PUBLIC_BRAND
In Next.js, any NEXT_PUBLIC_ var is inlined into the JS bundle at build time — using it would force a separate image build per client. Resolving the brand server-side from a plain BRAND env var (seeded into a React context + injected CSS variables in the server-rendered <head>) keeps one image for all brands.
NewBrandProvider seeded server-side in the root layout + a server-rendered <style> injecting --primary/--cta/--accent (no color flash).
Wire candidate-facing touch points only (the ~212 "Utkrusht" strings are mostly recruiter-facing — leave them): layout.tsx (title/OG/favicon), _AssessmentHeader.tsx, page.tsx, _header/Header.tsx (redirect), DeviceCheck.tsx (legal links), CrispChatbot.tsx (off for iuvius), the candidate certificatePDF.tsx.
Assets under public/brands/iuvius/. Env on the service: BRAND=iuvius, NEXT_PUBLIC_DOMAIN=assess.iuvius.com.
B · Backend bounded · no SES on critical path
Migration (file only, no push): organizations += custom_domain, branding_config (jsonb), suppress_platform_emails. Column-add → no new RLS.
Org-aware candidate-link base-URL (org.custom_domain) at the link sites in test_sessions.py, task_sessions.py, reminders.py, and the notifications microservice handlers.
Suppress platform candidate emails + reminders for the Iuvius org in notifications/core/dispatcher.py — this is what keeps any Utkrusht email off iuvius candidates (§2.3) while Jonathan sends invites himself.
PDF branding: thread org logo/name/color into PDFReportGenerator.py (report) and CertificateGenerate.py (certificate).
Add assess.iuvius.com to FLASK_CORS_ORIGINS; seed the Iuvius org row via DAO.
C · Recruiter app admin + questionnaire
Branded shared report — corrected (see §05). The recruiter /share/* route only brands a header and lives on a utkrusht.ai URL with Utkrusht chrome — it does not meet the requirement. The real approach is a /report/[token] route in the candidate app.
Surface a copyable iuvius-domain candidate link (using org.custom_domain) so Jonathan can distribute invites himself.
Invite Jonathan as admin via the existing team-invite flow — data isolation is automatic.
Questionnaire = existing primitive: enable is_ask_screening_questions and author positions.position_screening_questions (already shown to candidates at intake). ≈ 0 new code.
04
The build — ~3 weeks, one release
Delivered as a single release: nothing goes to Jonathan until the whole build (including the full branded report) passes internal UAT. The original "4 days" is now Milestone 1.
M0Start now
Inputs from Jonathan (external, non-blocking)
Logo / favicon / OG, brand hex colors, iuvius.com privacy + terms URLs, candidate intake questions, the CNAME for assess.iuvius.com, and confirmation he sends invites from iuvius.com. Optionally start SES domain-verification if we include automated sending.
M1~5 days
Base white-label
Candidate brand layer (src/brand/ + BrandProvider, touch points, certificate, Crisp off, assets); backend (org fields, org-aware links, email/reminder suppression, PDF + certificate branding, CORS, org seed); deploy to assess.iuvius.com (DNS+TLS); invite Jonathan as admin (access gated until release); author the questionnaire.
M2~3 days
Branded-report backend + share-gate
The linchpin: a backend share-gate (resolve token → shared_reports → OTP/email gate → short-lived view token, app-agnostic); report-data access for a non-owned shared session; the recruiter portal mints the assess.iuvius.com/report/<token> link for branded orgs.
M3~1.5–2 wk
Full-parity report + hardening → release
Candidate-app /report/[token] route rendered with Iuvius branding, reusing existing report views and porting the recruiter's rich analysis (buckets, sub-dimensions, prompt-trail, recommendation). Then §3 compliance pack, full E2E, data-siloing checks, onboarding runbook — internal UAT, then the single release to Jonathan.
05
The branded shared report (Option B)
Requirement: when a report is shared for a branded org, the link must open a fully Iuvius-branded page on an Iuvius URL — assess.iuvius.com/report/<token> — for whoever opens it, while the sharer keeps using the (internal, Utkrusht) recruiter portal.
Why the existing share route doesn't cut it
The recruiter /share/* page brands only its header, renders Utkrusht-green chrome, and lives on a utkrusht.ai URL. Grounded finding: the report is assembled and gated entirely inside the recruiter app (Supabase queries + an OTP / shareViewerToken gate), and Flask's report endpoint returns PDF only, no JSON. So the fix isn't cosmetic.
Backend share-gate (new — the linchpin): centralize share-resolution + access gate in the backend — resolve token → PUBLIC / INVITED / DOMAIN-restricted → OTP/email gate → issue a short-lived view token. App-agnostic, so any branded domain can use it.
Reuse what the candidate app already has: it already renders score, video, spider chart, timestamped evidence and competency cards for a candidate's own result — we extend that to fetch a shared session gated by the view token.
Full parity: port the recruiter's richer pieces — buckets, sub-dimensions, weighted spider, prompt-trail, recommendation — into the candidate app, themed with Iuvius branding.
Effort (grounded): MVP ≈ 4–5 days; full recruiter-parity ≈ 1.5–2 weeks — full parity is the chosen scope (Milestones 2–3), shipped in the single release.
06
Data compliance & repeatability
§3 — required, cheap now, painful if skipped
§3.6 disclosure (1 page): data in Supabase Mumbai ap-south-1, TUS, subprocessors (AI + cloud) — for iuvius's candidate privacy notice.
§3.3 siloing check: Iuvius candidates never appear in another recruiter's list.
White-Label in a Box
There are ~17 client folders in clients/ — this is a product, not a one-off. After this framework, onboarding a client ≈ 1 day: add a brand entry + assets → create the org (custom_domain + branding_config) → clone the Coolify service with BRAND=<client> + a CNAME + CORS origin → invite the admin → load their questionnaire → issue the compliance pack. Post-pilot: SES per client, Host-header brand selection (single service), a branding admin UI, and an API-subdomain proxy.
07
Risks
Risk
Sev
Mitigation
DNS/TLS external dependency on Jonathan's CNAME
med
Request on Day 0; Coolify auto-TLS.
Residual Utkrusht strings/chrome candidate-facing
med
Focused audit of the candidate flow only (not all 212 strings).
@iuvius.com automated sending (SES + DNS + prod-access ~24–48h)
med
Off the critical path — Jonathan sends invites for the pilot; SES lands after.
API hostname flapi.utkrusht.ai visible in devtools
low
Accept for pilot; API-subdomain proxy is post-pilot.
Scope creep into recruiter-console branding
low
Explicitly out of scope — not candidate-facing.
08
Verification — end to end
Candidate flow on assess.iuvius.com (via the /test-candidate-flow skill or manual): assert zero Utkrusht candidate-facing — tab title, header, footer, legal links → iuvius URLs, certificate branded, no Crisp, and no platform email sent (suppression on).
As Jonathan: create a position with screening questions → schedule a candidate → copy the assess.iuvius.com link → complete the assessment → view the report → download the iuvius-branded PDF → share the report link and confirm the viewer shows Iuvius logo/name.
Data siloing: the Iuvius candidate is visible only under the Iuvius org, in no other recruiter's list.
Regression: load app.utkrusht.ai (default brand) and confirm it is visually unchanged.
09
What we need from Jonathan
Everything required to start. Groups tagged blocker gate Day 1 or Day 3, so we chase those first; the rest can arrive alongside. This doubles as the reusable client-intake checklist for future white-label clients.
1 · Brand & visual identity blocker · Day 1
Logo files — primary (SVG preferred + PNG), a square "mark" for favicon/small spaces, and a white/monochrome version for dark backgrounds. Transparent background.
Exact brand colors as hex — the primary, plus any accent / call-to-action color. We map these to the app's primary / cta / accent.
Favicon & social preview — a favicon (or we derive it from the mark) and a 1200×630 share image (or we generate one).
App name string shown to candidates — e.g. "iuvius" or "iuvius Assessments".
Brand font — do you have a brand typeface, or should we approximate with a close neutral font for the pilot? (Custom webfonts may need licensing.)
Brand guidelines PDF, if one exists.
2 · Domain & DNS blocker · Day 3
Confirm the subdomain — assess.iuvius.com, or a different one?
Who controls iuvius.com DNS? Can they add a CNAME record we provide (and roughly how fast)? We hand over the target; propagation + TLS follow.
Any existing use of that subdomain that would conflict?
Primary technical contact on your side for the DNS + asset handoff.
3 · Candidate email & support
Confirm invite sending — for the pilot, iuvius sends assessment invites to candidates from your own iuvius.com mailbox. We provide the per-candidate assess.iuvius.com links. Which mailbox / sender name will you use?
OK to disable all our automated candidate emails (invite + reminders + completion) so no Utkrusht branding reaches candidates? (Recommended for §2.3.)
Candidate support contact — an email to show candidates who get stuck (replaces ours).
Candidate chat — keep it off (default, since our chat widget is Utkrusht), or wire your own chat workspace?
Post-pilot: do you want fully automated @iuvius.com sending later? That needs DKIM/SPF/DMARC records added to iuvius.com — is your IT/DNS able to?
4 · Legal & data blocker · Day 1 (URLs)
Privacy Policy & Terms URLs — iuvius-hosted, candidate-facing. Are they live now?
Data-location disclosure — confirm you'll add the disclosure we provide (data stored in India — Supabase Mumbai + named subprocessors) into your candidate privacy notice (§3.6).
Any data-residency requirement beyond India?
Retention — the defaults are recordings/logs ≤ 90 days, reports ≤ 12 months. Any earlier deletion you want?
Point of contact for candidate export/deletion requests (§3.7 / 3.8) on your side.
Any consent language you want shown before screen/camera recording?
5 · Candidate intake questionnaire blocker · Day 3
The questions candidates answer at intake — free-text, multiple-choice, or audio. Send the list, plus any "ideal answers" for grading.
Profile fields to collect — name, email, phone, resume, LinkedIn, GitHub, plus any custom fields.
Should intake responses be visible to iuvius reviewers in the report?
Same questionnaire across all roles, or per-role?
6 · Assessment setup
Which role(s) are you piloting — title, key skills/competencies, seniority band?
Assessment type — test (MCQ/coding), task-based (repo/build), or both?
Expected candidate volume and timeframe for the pilot.
Language — English (Hinglish is also supported)?
Recommendation threshold — the score cutoff above which a candidate is "recommended".
7 · Reports & access
Who needs report access — emails to invite (and at what role).
Downstream sharing — who will you share reports with (your own clients / hiring managers)? The shared-link viewer will carry iuvius branding.
PDF — confirmed you want downloadable PDF reports; any specific fields or layout you need on them?
8 · Admin, team & logistics blocker · Day 3
Jonathan's email for the admin invite; any additional team members to add now and at what role.
Target go-live date for the pilot.
Completion certificate — do candidates get one? If yes, the issuer name to print (e.g. "iuvius").
Draft for review · Iuvius White-Label Pilot · Utkrushta Learning Services · generated for internal planning