Last updated: August 2026 This document describes what Navly is, what it does, how it is built, and how each part of the system works together.
Navly is a Canadian immigration planning and tracking app — not an immigration consultant.
It helps users:
- Understand which Canadian PR pathways may be possible based on their profile
- Estimate their CRS score and FSW 67-point grid result
- Track physical presence in Canada (days, streak, travel log)
- Monitor permit expiry, PR card, passport, and language test deadlines
- Match against Provincial Nominee Program (PNP) streams
- Track progress toward citizenship or PR residency obligation
- Ask general immigration questions through an AI assistant
- Connect with certified Canadian immigration consultants
Navly never collects documents, passport numbers, SINs, or official government IDs. It collects only user-entered profile data.
| Navly does | Navly does not |
|---|---|
| Estimate CRS / CLB / FSW scores | Provide legal advice |
| Rank possible pathways | Guarantee any outcome |
| Track Canada presence days | Replace an RCIC or immigration lawyer |
| Alert on permit / deadline expiry | Review documents |
| Match PNP streams by province | Submit applications |
| Explain immigration terms with AI | Collect passports, SINs, or official IDs |
| Connect users with certified consultants | Claim to be an IRCC service |
| Layer | Technology |
|---|---|
| Framework | Next.js App Router (TypeScript) |
| Styling | Tailwind CSS v4 |
| UI components | shadcn/ui (Radix primitives) |
| Database & Auth | Supabase (PostgreSQL + Auth) |
| AI / LLM | Groq API — model openai/gpt-oss-120b |
| Payments | Stripe (Checkout + webhooks) |
| Analytics | PostHog |
| Deployment | Vercel (implied by Next.js App Router) |
| Icons | Lucide React |
| Internationalization | Custom lib/i18n.tsx with English + French locales |
app/ — Next.js App Router pages and API routes
page.tsx — Public landing page
onboarding/page.tsx — Onboarding flow entry
dashboard/ — All authenticated dashboard pages
page.tsx — Main dashboard
days/page.tsx — Canada presence tracker
citizenship/page.tsx — Citizenship physical presence calculator
residency/page.tsx — PR residency obligation tracker
dates/page.tsx — Deadlines & key dates
chat/page.tsx — AI immigration assistant
news/page.tsx — EE draws + immigration news
consultants/page.tsx — Consultant directory
tasks/page.tsx — Settlement task list
report/page.tsx — Readiness report (paid)
pr-tracker/page.tsx — Full PR pathway tracker (paid)
prep/page.tsx — Application prep checklist
profile/page.tsx — Profile editor
admin/ — Admin pages (consultants, updates)
api/
chat/route.ts — AI chat endpoint (Groq streaming)
checkout/route.ts — Stripe checkout session creator
webhook/route.ts — Stripe webhook handler
draws/route.ts — EE draws endpoint
news/route.ts — Immigration news endpoint
auth/check-phone/ — Duplicate phone check
admin/ — Admin seed and management routes
cron/ — Scheduled jobs (news sync, EE draws sync, reminders)
components/
onboarding/
IntakeFlow.tsx — Onboarding orchestrator
flow.ts — Dynamic step list (getSteps)
validation.ts — canContinue + hint per step
steps/ — One file per step group
dashboard/
Sidebar.tsx — Desktop sidebar nav
BottomNav.tsx — Mobile bottom nav
ActionableScoreSheet.tsx
ScoreTimelineChart.tsx
ProgressGauge.tsx
RequirementCard.tsx
DocumentStorage.tsx
ui/
Navbar.tsx — Public nav
PlanGate.tsx — Paywall wrapper
UpgradeModal.tsx
LanguageSwitcher.tsx
lib/
profile.ts — IntakeData type, EMPTY_PROFILE, localStorage + Supabase sync
presence.ts — PresenceData type, check-in, travel log, streak, cloud sync
scoring.ts — CLB conversion, CRS calculation, FSW 67-pt, pathway assessment
pr-math.ts — PR residency obligation + citizenship presence calculation
pnp.ts — PNP stream matching engine (BC, ON, AB, SK, MB, Atlantic)
deadlines.ts — Deadline computation from profile dates
draws.ts — Express Entry draw history + cutoff data
crs-tables.ts — CRS point tables (age, education, language, Canadian work)
rough-crs.ts — Quick rough CRS estimate for pre-signup preview
subscription.ts — usePlan hook, plan checks (tracker / report)
tasks.ts — Settlement task list with defaults
consultants.ts — ConsultantListing type
news.ts — Immigration news fetching
settlement-funds.ts — IRCC settlement fund requirements
ircc-rss.ts — IRCC RSS feed parser
i18n.tsx — Internationalization (EN / FR)
analytics.ts — PostHog track + identify wrappers
geo.ts — Geo detection helpers
history.ts — Profile history tracking
supabase/
client.ts — Browser Supabase client
server.ts — Server-side Supabase client
rules/ — Versioned immigration rule snapshots
clb/ — CLB conversion tables (IELTS, CELPIP, PTE, TEF, TCF)
crs/ — CRS additional points table
loader.ts — getActiveRule() — selects the most recent rule version
Users complete a short questionnaire before creating an account. This lets them see their plan preview before committing. Sign-up is the final step.
Step sequence (dynamic, driven by components/onboarding/flow.ts):
goal-first "What do you need help with today?"
Options: deadlines / pr / citizenship / residency / explore
location-split "Are you in Canada right now?"
Options: inside / outside
[inside only]
inside-status Current immigration status
Options: student / work-permit / visitor / refugee /
family-member / out-of-status / pr / other
[outside only]
planned-entry Planned route to Canada
Options: study-permit / work-permit / visitor /
express-entry / family / business / unsure
[inside non-PR with known status]
key-date One key date relevant to their status
(permit expiry for workers/students/visitors, PR date for PRs)
[all users except PR]
quick-crs Quick scoring inputs: age (birth year + month), education level,
self-reported CLB, Canadian work months, foreign work years
plan-preview Shows rough CRS estimate + what Navly will track for them
early-signup Account creation: email, password, phone verification
The step list is recalculated dynamically after every field change. Steps are added or removed based on data.locationStatus, data.status, and other answers.
Profile data is stored in localStorage throughout onboarding and synced to Supabase after login.
After login, users land on the main dashboard. The dashboard:
- Syncs profile from Supabase on mount (
syncProfile) - Syncs presence data from Supabase on mount (
syncPresence) - Calculates score (
calculateScore) from the loaded profile - Shows CRS strength (Competitive / Developing / Below target) vs the latest EE draw cutoff
- Shows Canada presence day count + check-in button
- Shows urgent deadlines (permit expiry within 60–180 days)
- Shows incomplete tasks count
- Links to all sub-pages
Dashboard sub-pages:
| Route | Purpose |
|---|---|
/dashboard/days |
Canada presence tracker — daily check-in, travel log, streak |
/dashboard/citizenship |
Citizenship physical presence calculator (1,095-day rule) |
/dashboard/residency |
PR residency obligation tracker (730-day rule) |
/dashboard/dates |
All deadlines with status and action links |
/dashboard/chat |
AI immigration assistant |
/dashboard/news |
Express Entry draw history + immigration news |
/dashboard/consultants |
Certified consultant directory |
/dashboard/tasks |
Settlement task list |
/dashboard/report |
Readiness Report PDF (paid: report or tracker plan) |
/dashboard/pr-tracker |
Full pathway + score breakdown (paid: tracker plan) |
/dashboard/profile |
Edit intake profile |
/dashboard/prep |
Application preparation checklist |
All user-entered immigration data lives in a single flat TypeScript type IntakeData in lib/profile.ts.
Major field groups:
| Group | Fields |
|---|---|
| Identity | fullName, email, phone, gender |
| Primary use | primaryUse (deadlines / pr / citizenship / residency / explore) |
| Location | locationStatus, status, plannedEntry, province, city, arrivalDate, visaExpiryDate |
| Goal | goal, timeline |
| Personal | age, birthYear, birthMonth, maritalStatus, spouseComing |
| Spouse | spouseLangTestType, spouseLangScores, spouseEducationLevel, spouseCanadianWorkMonths |
| Language | langTestType, langTestDate, langReading/Writing/Listening/Speaking |
| Second language | lang2TestType, lang2Scores |
| French | frenchTestType, frenchScores |
| Education | educationLevel, canadianEducation, ecaCompleted, ecaIssueDate, ecaExpiryDate |
| Work | noc, teerLevel, foreignWorkYears, canadianWorkMonths, hasJobOffer, wage, hoursPerWeek |
| Settlement | familySize, settlementFunds |
| Family ties | canadianSibling, manitobaFamilyRelative, parentOrChildSponsor, relativesInCanada |
| PNP | pnpNomination, pnpJobOfferProvince, pnpEducationProvince, employerSupportsPNP, etc. |
| Risk flags | previousRefusals, lostStatus, criminalityIssues, removalOrder, medicalInadmissibility |
| Student | programLevel, programLengthMonths, schoolName, dliNumber, pgwpApplied, pgwpExpiry, etc. |
| Worker | workPermitType, permitExpiry, lmiaNumber, workStartDate, fullTimeOrPartTime, etc. |
| PR | prDate, prCardExpiry, prPreStatus, taxFilingComplete, citizenshipLangProof, etc. |
| Dates | passportExpiry, visitorRecordExpiry |
| Preferences | reminderOptIn |
| Quick onboarding | selfReportedCLB |
| Sync metadata | _updatedAt (ISO timestamp for conflict resolution) |
Storage: localStorage key navly_profile + Supabase profiles.profile_data (JSONB column).
Sync strategy: _updatedAt inside profile_data is the single source of truth. Whichever record has the newer _updatedAt wins. The DB updated_at column is audit metadata only and is never used for conflict resolution.
Physical presence tracking data lives in lib/presence.ts.
PresenceData {
totalDays: number // confirmed days in Canada
streak: number // current consecutive days
longestStreak: number
lastCheckIn: string | null // 'YYYY-MM-DD'
lastAcknowledgedDate: string | null
arrivalDate: string | null
travelLog: TravelEntry[] // each trip outside Canada
deletedTravelIds: string[] // IDs propagated for multi-device delete sync
_updatedAt: string
}
TravelEntry {
id: string // crypto.randomUUID()
departureDate: string
returnDate: string // empty if still away
country: string
reason: string
createdAt: string
updatedAt: string
}Storage: localStorage key navly_presence + Supabase profiles.presence_data (JSONB column).
Sync strategy: Travel logs are union-merged by ID — newest updatedAt per entry wins. Deleted IDs are combined from both sources so deletes propagate cross-device. Scalar fields (streak, totalDays, etc.) come from whichever record has the newer _updatedAt.
| Table | Purpose |
|---|---|
profiles |
One row per user — profile_data (JSONB) + presence_data (JSONB) |
subscriptions |
Active plans — user_id, plan (tracker/report), status, expires_at |
consultants |
Consultant directory — name, province, languages, services, booking_link, sponsored, verified |
immigration_news |
Synced immigration news — title, summary, source_name, source_type, published_at, category |
rule_snapshots |
Versioned IRCC rule data — rule_key, category, data (JSONB), source_url, effective_date, status |
All scoring lives in lib/scoring.ts with point tables in lib/crs-tables.ts.
Raw test scores are converted to Canadian Language Benchmark (CLB) levels per skill (reading, writing, listening, speaking).
Supported tests:
- IELTS General Training — lookup tables per skill
- CELPIP-General — 1:1 mapping (score IS the CLB, capped at range)
- PTE Core — lookup tables per skill
- TEF Canada — lookup tables per skill
- TCF Canada — lookup tables per skill
Tables live in rules/clb/ and are loaded via getActiveRule() so they can be updated with a new version when IRCC publishes new equivalencies.
CRS Total = age + education + firstLanguage + secondLanguage +
spouseFactors + canadianExperience + skillTransferability + additional
| Component | Max (with spouse) | Max (no spouse) |
|---|---|---|
| Age | 100 | 110 |
| Education | 140 | 150 |
| First language | 128 | 136 |
| Second language (French) | 50 | 50 |
| Spouse factors | 40 | — |
| Canadian experience | 70 | 80 |
| Skill transferability | 100 | 100 |
| Additional (PNP, sibling, Canadian education) | 600 | 600 |
Key rule: Job offers no longer add CRS points as of March 25, 2025. The arranged employment factor is 0 in the active CRS rule.
French bonus: CLB 7+ French with CLB 5+ English = 50 extra CRS points. CLB 7+ French alone = 25 points.
Age: Computed dynamically from birthYear + birthMonth so it auto-updates when the user's birthday passes — not frozen at a typed number.
Canadian work hours cross-check: If hoursPerWeek is provided, the app computes both months-based and hours-based (at 1,560 hrs/year) year equivalents and uses the more conservative of the two.
FSW Total = language + education + workExperience + age + jobOffer + adaptability
| Factor | Max |
|---|---|
| Language (CLB 7 minimum) | 28 |
| Education | 25 |
| Work experience | 15 |
| Age | 12 |
| Job offer | 10 |
| Adaptability | 10 |
Pass mark: 67/100. Also requires CLB 7+ in all 4 skills and 1 year of continuous TEER 0–3 skilled work.
Note: Job offer still gives 10 FSW selection-factor points — it just no longer adds CRS points.
Settlement funds are required for FSW unless the user is currently authorized to work in Canada with a valid job offer (funds exemption).
The main calculateScore() function returns a ScoreResult containing:
{
clb: CLBScores | null
crs: CRSBreakdown | null
fsw: FSWResult | null
pathways: PathwayStatus[] // status: 'eligible' | 'not-yet' | 'possible' | 'not-applicable' | 'high-risk'
improvements: Improvement[] // specific next actions with CRS impact
riskFlags: RiskFlag[] // warnings and critical flags
hasEnoughData: boolean
missingFields: string[]
}Pathways assessed:
| Pathway | Key requirements |
|---|---|
| Canadian Experience Class (CEC) | 1yr TEER 0-3 Canadian work, CLB 7 (TEER 0/1) or CLB 5 (TEER 2/3), authorized work only |
| Federal Skilled Worker (FSW) | CLB 7+, 1yr continuous TEER 0-3, 67+/100, settlement funds |
| Federal Skilled Trades (FST) | TEER 2/3, CLB 5 speaking/listening + CLB 4 reading/writing, 2yr trades work, job offer or certificate |
| PGWP → CEC pathway | DLI, 8+ month program, eligible program, language proof, field-of-study check (2026 rules) |
| Study permit extension | Shown for students |
| Work permit extension + BOWP | Shown for work permit holders |
| Visitor pathways | Visitor status, visitor-to-work/study permit exceptions |
| Maintained status | Shown if user applied before status expired |
| Rural Community Immigration Class | Job offer from designated rural employer, 1yr TEER 0-3, CLB 4-6 (TEER-dependent), high school min |
| Provincial Nominee Program | Matched by intended province (see PNP engine below) |
Risk flags built automatically:
- Lost status / overstay → critical
- Previous refusal → warning
- Visitor trying to work without exception → warning
- Student counting full-time study work toward CEC → warning
- FSW user with no settlement funds and no job offer → warning
lib/pnp.ts matches the user's profile against provincial streams.
Supported provinces and streams:
| Province | Streams |
|---|---|
| BC | Skilled Worker, International Graduate, Express Entry BC (200-pt grid) |
| ON | Employer Job Offer (Foreign Worker), Employer Job Offer (International Student), Human Capital Priorities |
| AB | Alberta Opportunity Stream, Express Entry Stream |
| SK | Employment Offer, Occupations In-Demand (100-pt grid + NOC list) |
| MB | Skilled Workers in Manitoba, Skilled Workers Overseas |
| NS / NB / PE / NL | AIP Skilled Worker, AIP International Graduate (readiness checklist model) |
| QC | Not supported — directs users to immigration.quebec.gouv.ca |
When intendedProvince === 'Any', all provinces are evaluated and results can be grouped by province in the UI.
Each stream returns:
status: eligible / possible / not-yet / not-applicablereason: one-line explanationmissingItems: actionable list of what's blockingscore/maxScore: numeric grid estimate (BC, SK) or connection strength indicator (ON, AB, MB)readinessItems: binary checklist display (AIP)
BC uses a 200-point internal grid (job offer, work experience, education, language, adaptability). SK uses a 100-point grid (experience, education, language, age, SK connection). ON, AB, MB use a 0–5 connection strength indicator (not a disclosed point grid). AIP uses a 3-item binary readiness checklist (designated employer, language, education).
lib/pr-math.ts handles the two major physical presence calculations for PR holders.
IRCC rule: physically present in Canada for at least 730 days in any 5-year rolling period.
The window starts from the later of (PR date, 5 years ago) and runs to today.
Days outside Canada are subtracted using the travelLog from PresenceData.
Return day is counted as IN Canada; departure day is counted as OUT (IRCC convention).
Returns:
daysInCanada,daysRequired(730),daysNeeded,daysBufferstatus:on_track|at_risk|breachestimatedMeetDate: when 730 is reached assuming no future travel
IRCC rule: physically present in Canada for at least 1,095 days in the 5 years before the citizenship application date.
Pre-PR days (as temp resident within the 5-year window) count as half-days, capped at a maximum credit of 365 days.
totalCreditedDays = postPRDays + min(floor(prePRDays / 2), 365)
Returns:
postPRDays,prePRDays,prePRCredit,totalCreditedDaysdaysRequired(1095),daysNeeded,pctCompletestatus:eligible|on_track|needs_more_time|incomplete_dataestimatedEligibilityDate: when 1095 credited days will be reached
lib/presence.ts powers the daily check-in system in app/dashboard/days.
checkIn()— confirms today as a Canada day, increments streak if consecutivegetMissedDays()— returns up to 30 unconfirmed days between last acknowledged date and yesterdayconfirmMissedDay(date)— marks a past day as "in Canada"declineMissedDay(date)— marks a past day as "outside Canada", breaks streak if consecutivecomputeStreak()— calculates current streak from arrival date + travel log (not dependent on check-ins alone)getDaysInCanada()— total confirmed Canada days = (days since arrival − travel days), credits today only if checked in
Each trip outside Canada is stored with departure date, return date, country, and reason.
getTravelDays() sums all travel log entries, excluding the return day.
Open trips (no return date) are counted as still ongoing to today.
lib/deadlines.ts computes all upcoming deadlines from the user's profile.
Deadline types tracked:
work_permit— work permit expirystudy_permit— study permit expirypgwp— PGWP expiryvisitor_record— visitor record expirypassport— passport expiry (for renewal reminders)language_test— language test 2-year validity (EE requirement)pr_card— PR card expiry
Alert schedule: reminders fire at 180, 120, 90, 60, 30, and 7 days before expiry.
Deadline status thresholds:
on_track— more than 180 days awayaction_soon— 61–180 days awayurgent— 1–60 days awayexpired— already past
YYYY-MM inputs (permit expiry) are normalized to the last day of that month to give the user the maximum benefit of the doubt.
app/api/chat/route.ts powers the AI assistant in app/dashboard/chat.
- LLM: Groq API, model
openai/gpt-oss-120b, streaming response - Auth gate: requires active Supabase session
- Rate limit: 20 messages per user per minute (in-memory)
- Context window: last 14 messages (7 turns) are sent to the model
- RAG: retrieves relevant rule snapshots and recent news from Supabase before each request
The query is keyword-matched against immigration topics:
| Keywords | Category pulled |
|---|---|
| express entry, CRS, EE draw, cutoff | express_entry + latest_ee_draw |
| PGWP, post-grad, work permit | pgwp |
| citizenship, naturalize | citizenship |
| PR card, residency obligation | pr_residency |
| settlement fund, proof of funds | proof_of_funds |
| French, bilingual | express_entry |
| PNP, provincial nominee | express_entry |
Up to 10 active rule snapshots are injected into the system prompt. The 8 most recent immigration news items are also included (excluding Quebec content).
The user's full profile is summarised and injected into the system prompt so the AI can personalise answers. The summary includes: status, origin, location, goal, age, marital status, language scores + CLB equivalents, education, TEER level, work history, job offer status, permit expiry, risk flags, family size, estimated CRS, FSW score, and pathway match summary.
Critical overrides applied before any other logic:
- Quebec: redirects to immigration.quebec.gouv.ca — no further answer
- Off-topic: responds with "I can only help with Canadian immigration questions"
- High-risk legal situation (removal order, misrepresentation, criminal charge, detention, refugee claim, out-of-status): stops and escalates to RCIC/lawyer
Banned phrases: "You qualify", "You are eligible", "You will be approved", "You should apply", "You do not need a lawyer", "Submit this application", "You are safe to stay", etc.
Required safe replacements: "Based on the data you entered, this pathway may be possible", "This pathway appears to match your profile — a certified consultant should confirm", etc.
All specific rules, fees, or thresholds must be cited with source and effective date.
lib/draws.ts contains the complete history of Express Entry draw results as a static array.
Each draw entry:
{ date: string, type: string, cutoff: number, invited?: number }Draw types: All programs, Canadian Experience Class, Federal Skilled Worker, Provincial Nominee Program, French Language Proficiency.
The draw data is hand-maintained via scripts/add-draw.ts. The DRAWS_LAST_UPDATED constant tracks when data was last verified.
The latest draw cutoff is shown on the main dashboard to compare against the user's CRS score.
Navly has three pricing tiers:
| Plan | Price | Type | Features |
|---|---|---|---|
| Free Check | $0 | Forever free | Basic CRS estimate, FSW check, basic pathway overview, consultant directory |
| Readiness Report | $69.99 | One-time payment | Full CRS + FSW breakdown, Top 3 pathways, gap analysis, PNP match, PDF report |
| PR Tracker | $119.99/yr or $14.99/mo | Subscription | Everything in Report + presence tracker, deadline alerts, EE draw alerts, monthly recalculation, AI assistant |
Plan gates are implemented via components/ui/PlanGate.tsx and lib/subscription.ts.
// Check plan access
const { plan } = usePlan()
hasReport(plan) // true for 'report' or 'tracker'
hasPlan(plan, 'tracker') // true only for 'tracker'POST /api/checkout creates a Stripe Checkout session:
plan: 'report'→ one-time payment (mode:payment)plan: 'tracker'withbilling: 'monthly' | 'annual'→ subscription (mode:subscription)- Tracker subscription includes a 7-day free trial
- Promotion codes are allowed on all plans
client_reference_idis set to the Supabase user ID for webhook matching
POST /api/webhook handles Stripe events:
checkout.session.completed→ writes row tosubscriptionstable with plan, status, and expirycustomer.subscription.deleted→ marks subscription inactive
Supabase Auth handles login, signup, session management, and password reset.
lib/supabase/client.ts— browser-side Supabase client (singleton pattern)lib/supabase/server.ts— server-side Supabase client (for API routes and Server Components)
Duplicate account prevention:
POST /api/auth/check-phone— checks if a phone number is already registered before SMS verification- One phone number = one account; one email = one account
Phone verification uses Supabase's built-in OTP flow.
Profile and presence data use a local-first, optimistic sync model:
User edits → localStorage (instant, always works offline)
→ Supabase (async, non-blocking)
On login / app load:
syncProfile(userId) — compares _updatedAt, winner is written to both stores
syncPresence(userId) — travel logs are union-merged, scalar fields use newer timestamp
This means:
- The app works fully offline
- Multi-device is supported (cloud resolves conflicts)
- A server-side DB touch (like a migration) cannot silently overwrite a newer client record
- Travel log deletions propagate via the
deletedTravelIdsarray
lib/i18n.tsx provides English and French translations.
Locale files: lib/locales/en.ts and lib/locales/fr.ts.
The useLocale() hook returns a t(key) translation function and the current locale. Language is switchable via components/ui/LanguageSwitcher.tsx.
lib/analytics.ts wraps PostHog with two functions:
track(event, properties)— logs an eventidentify(userId)— links events to a user
Key tracked events:
dashboard_viewedapp_openedonboarding_*(step completions)checkin_completedchat_message_sent
PostHog is initialized via components/PostHogProvider.tsx.
Scheduled API routes (app/api/cron/):
| Route | Purpose |
|---|---|
cron/news/route.ts |
Fetches new immigration news, deduplicates, stores in immigration_news |
cron/sync-ee-draws/route.ts |
Syncs latest EE draw data |
cron/sync-sources/route.ts |
Syncs IRCC RSS feeds via lib/ircc-rss.ts |
cron/reminders/route.ts |
Sends deadline reminder emails to opted-in users |
Admin pages at /admin/:
- Consultants — create, edit, verify, sponsor, activate/deactivate consultant listings
- Updates — manage rule snapshots and immigration news
Admin API routes:
POST /api/admin/consultants— CRUD for consultant listingsPOST /api/admin/updates— manage rule snapshotsPOST /api/admin/seed-news— seed initial news dataPOST /api/admin/seed-rules— seed rule snapshot dataPOST /api/admin/test-rss— test IRCC RSS parsing
lib/consultants.ts defines ConsultantListing:
{
id, name, business_name, certification_type, license_number,
city, province, languages[], services[], booking_link,
avatar_url, contact_email, sponsored, verified, active, agency_code
}The landing page always fetches one sponsored + verified + active consultant to display as a featured ad. The /dashboard/consultants page shows the full searchable directory.
Legal disclaimer always shown: "Independent professional. Navly does not provide immigration consulting services and is not responsible for services offered by listed consultants."
lib/tasks.ts provides a default settlement checklist for newcomers:
| Category | Default tasks |
|---|---|
| Settlement & Living | Find housing, learn where to shop, driver's license exchange |
| Taxes & Finance | File first tax return, build Canadian credit history |
| Immigration | (generated dynamically from profile) |
| Arrival Checklist | (status-specific) |
Tasks are stored in localStorage (navly_tasks). Users can add custom tasks and mark items done.
Immigration rules change frequently. Navly handles this with a versioned rules loader:
rules/clb/— CLB conversion tables. Each version has averifiedDate.getActiveRule()picks the most recent.rules/crs/— CRS additional points table (PNP = 600, Canadian education = 15/30, sibling = 15, arranged employment = 0 as of 2025-03-25).rule_snapshotsSupabase table — live rule data pulled for the AI chat (settlement funds, EE draw history, PGWP requirements, citizenship rules, etc.)
When IRCC updates a rule, a new version is added to rules/ and the AI rule snapshot is updated in Supabase. No code change is needed for the AI to use updated data.
The following rules are enforced throughout the entire app — in UI copy, in the AI system prompt, and in the product boundary:
- Navly never says "You qualify", "You are eligible", or "You will be approved"
- All eligibility language uses "may be possible", "appears to match", "based on the data you entered"
- Every eligibility screen recommends consulting a certified RCIC or immigration lawyer
- High-risk profiles (criminal issues, removal orders, misrepresentation, out-of-status) are stopped from the normal flow and shown a professional review warning
- No documents are collected — no passport, no SIN, no birth certificate, no bank statements
- Presence tracker includes the disclaimer: "This tracker is for personal planning only. Final physical presence calculations depend on official government records."
- Settlement fund amounts are sourced from IRCC 2024 tables and shown with a last-verified date
- PGWP rules display a last-verified date and always link to canada.ca
- Job offer wording: "A job offer no longer adds CRS points as of March 25, 2025. It may still strengthen PNP streams."
- Student work wording: "Work while studying full-time does not count for CEC. Post-graduation PGWP work may count."