Every portal query is scoped to one client and nothing leaks
The portal query layer is a single TypeScript file with seven exported functions. Every function takes a clerkId as its first argument. Every function starts by looking up the client record with that clerkId. If no client exists, it returns null or an empty array. No data is ever returned without first proving that the requesting user owns it. The getClientDashboard function uses a single Prisma query with nested includes — projects filtered to active and in-review statuses, milestones ordered by sort position, the latest session with its comments, and the next upcoming booking. One query, one round trip, one render. The getProjectDetail function takes a project ID and a clerkId. It finds the client first, then queries the project with a where clause that checks both the project ID and the client ID. If someone tries to access a project that belongs to a different client, the query returns null and the page renders a 404. The getClientBookings function splits results into upcoming and past using a Promise.all with two parallel Prisma queries — one for bookings after now ordered ascending, one for bookings before now ordered descending and limited to ten. The Stripe invoice query works differently because invoices live in Stripe, not the database. The getClientInvoices function takes a stripeCustomerId, hits the Stripe API directly, filters to succeeded and pending intents, and formats the response with Intl.NumberFormat using the actual currency code from each payment. Seven functions serve the entire portal. The data access pattern is always the same — authenticate, look up the client, scope the query, return the result. Adding a new portal page means calling an existing query function or adding one to the same file.
Comments coming soon
Sign in with TikTok to leave a comment. Coming soon.