Accounts & roles

Every account is one of three roles, chosen at signup. The role determines which product you land in and cannot be changed afterwards without creating a new account.

RoleLands onHas a student workspace
Student/dashboardYes
Counselor/schoolsNo
Organization/organizationsNo

Student

The default. Lands on /dashboard after onboarding. A student owns their own data: their college list, essays, activities, documents and scores. Nobody sees any of it unless the student invites them or a counselor holds them on a roster.

Joining a school

Students may enter a join code at signup, which links them to that school's roster. The code is generated when a counselor creates their school and looks like LINCOLN26. Joining a school is optional, the student product works identically without one, minus counselor assignments.

A student who joins a school shares their progress with that counselor, activity counts, college list, essay status, GPA. They never share essay draft text or documents. See Privacy.

Clerk's hosted sign-up cannot carry a role, so it is collected once afterwards at /welcome. The For Schools and For Organizations calls to action carry ?role=counselor or ?role=organization, so the sign-up page shows the right framing instead of XP and an avatar, and /welcome arrives with that role already selected.

That intent cannot live in the URL alone. Clerk uses path routing under /signup/[[...rest]], so after the first step it navigates to /signup/verify-email-address, and Google sign-up leaves the site entirely and returns via /signup/sso-callback. Neither carries the original query, so the sub-route recomputed a plain /welcome and dropped counselors into the student quiz. proxy.ts therefore records the intent in a one-hour ud_role_intent cookie, which /welcome reads as a fallback and completeWelcome deletes once consumed. It is presentational only: the role written to the database is whatever the user submits.

Counselor

Lands on /schools. On first login a counselor creates their school, which generates the join code. From there they get the overview page and the enterprise console.

A counselor has no student workspace of their own, no college list, no essays. Their account exists to observe and support the students on their roster.

Organization

Lands on /organizations, the owner console. Signing up with an organisation email is the intended path, and the listing itself captures a separate contact_email for application alerts, which is shown to the owner only and never appears on the public listing.

An organisation account has no college list, no essays and no roster. It exists to run one or more listings and read the applicants they attract. The console shows the organisations you own first, then the rest of the directory as a table so you can see where you sit among them. See Organizations.

Organisation accounts can browse Open to Work, because recruiting is the whole point of the directory. Counselors cannot: they already have their own roster, and a browsable directory of other schools' students is a different product with a different consent question behind it.

Contributors

Parents, mentors and outside counselors do not need a special role. They sign up as a normal account and accept an invite link from a student. That grants them read (or read-and-comment) access to that one student, surfaced at /shared. See Contributors.

Authentication

  • Identity is Clerk. Fledgy AI stores no passwords and no password hashes, and never sees a credential. Email verification, password reset, multi-factor and rate limiting are Clerk's to provide.
  • The local users row is keyed to the Clerk account by clerk_user_id. Everything else, XP, role, streak, is ours.
  • Sessions are Clerk's cookies, validated on the server by proxy.ts before any page renders.

Not yet implemented: deleting a Clerk account does not yet cascade to the local row, and there is no self-serve export or erase flow. Both are required before any real deployment.

Which routes are gated

proxy.ts gates /dashboard, /onboarding, /welcome, /marketplace/sell and the counselor console at /schools, /schools/console, /schools/settings and /schools/students. It redirects rather than throwing, so a signed-out visit is a redirect to sign-in and not a server error, and it carries the counselor intent when the request was headed for the console.

/schools/about and /schools/pricing are deliberately public: they are what a school reads before it has an account. They were previously swept up by a /schools(.*) matcher, which meant the only people who could read the pricing were the ones who had already bought.

The gate is optimistic only. Real authorization lives in every page and Server Function via currentUser(), because a Server Function is a POST to the route that declares it and a matcher change could silently drop coverage.

Streaks

Every authenticated request checks the date of your last visit. Same day is a no-op; the previous day increments your streak; anything older resets it to 1. There is no way to backfill a streak.