Everything a student wants to know, how far through the program they are, whether they are on pace, what is due next, usually already lives in your CRM. The hard part is showing it to them without showing them anyone else's data. Here is how we built a student self-service portal on a public Salesforce Site, and the parts that mattered most.
Why a public Site, not a full login
You could give every student a Salesforce login. For a large cohort that is licenses, password resets, and a support burden most schools do not want. A public Salesforce Site is the lighter path: one page, reachable by anyone, that identifies the student and shows only their record. The catch is obvious and it is the whole design problem. If anyone can reach the page, the page itself has to be the thing that keeps every student to their own data.
The guest user is the entire security model
A public Site runs as a single guest user. Whatever that guest user can do, the internet can do. So the rule is least privilege, taken seriously:
- Grant the guest user the page and its controller, and nothing else. No object permissions at all.
- Let the controller decide, in code, exactly which record to load and which fields to return, after it has identified the student.
- Never lean on the guest user's object access to fetch data, because the safest amount of standing access for a public page is none.
Get this right and the portal cannot leak, because there is nothing to leak: the guest user holds no keys to the data, the controller hands out one student's view after it has proven who they are.
One engine, two contexts
The progress calculation the portal shows is the same one the coaching team already uses. We did not want two copies drifting apart, so we made one class serve both, in different access contexts.
The trick is to move the class from with sharing to inherited sharing, and pass an explicit access level into each query. The coach component runs it in user mode, so a coach sees exactly what their permissions allow. The portal runs the identical calculation in system mode, because the guest user has no record access of its own and the controller has already scoped the query to the signed-in student. One engine, two callers, no second copy of the maths to keep in step.
Sign-in without a password, and the bug that would have locked everyone out
Students sign in with something they know rather than a password, and every attempt is written to a log so repeated wrong guesses can be throttled. That throttle is where the interesting bug lived.
The throttle counts failures per client. Our first version keyed the client on a value that was always blank, because the sign-in ran as a Visualforce remote action, and remote actions carry no request headers. Every attempt looked like it came from the same unknown client. Six wrong guesses from anyone would have throttled everyone, locking every student out for the window.
The fix was to stop using a remote action for sign-in and use a page postback instead, which does carry the real client IP. One tempting shortcut, reading the source IP from the session, throws an uncatchable exception when there is no session, which is exactly the guest case, so the tests caught it before it shipped. The lesson: on a public page, assume there is no session and no header until you have proven otherwise.
The date bug nobody notices until a student in another timezone does
The schedule shows each course's due date, calculated from the student's program start date. Date-only values have no timezone, but parse one as a timestamp and it becomes midnight UTC, which is the day before for anyone west of Greenwich. Students in the US saw every due date one day early. The fix is to read date-only fields as local calendar days, not as instants. It is a small bug with an outsized effect on trust, because a portal that shows the wrong date is a portal students stop believing.
What the student sees
Once the plumbing is right, the page is calm: the student's progress against the expected pace, their standing in plain language, their class schedule with the next courses due highlighted, the messages the school has sent them, and a short read on what it would take to get back on track. Everything they used to email a coach to ask, answered the moment they sign in.
Frequently asked questions
Can a public Salesforce Site show a user only their own records?
Yes, if the guest user holds no object permissions and the controller loads a single record after identifying the user. The safety comes from the code scoping every query to the signed-in person, not from the guest user's access.
How do you reuse a sharing-aware Apex class on a public page?
Change it to inherited sharing and pass an explicit access level into each query. Staff-facing callers run it in user mode; the public page runs it in system mode after the controller has already scoped the query to the signed-in user.
Is a date-of-birth sign-in secure enough for a portal?
On its own it is a low bar, so pair it with a throttle: log every attempt and lock out repeated failures. Make sure the throttle keys on a real client identifier, because a public page's requests can all look identical if you key on the wrong thing.
Why did dates show up one day early for some students?
Date-only values parsed as timestamps become midnight UTC, which is the previous calendar day for users in western timezones. Read them as local calendar days instead.
From an inbox to a portal
We built this for a university whose Success Coaches were answering the same progress questions by hand. Now students see their trajectory, schedule, and standing themselves. The full story is in our student portal case study.
If the answers your users keep asking for already live in Salesforce, we can surface them safely in a portal. Book a free consultation.