- What is the first thing to get right in a client portal?
- The scoping, before any feature. Create a clients table, a membership joining people to clients, and a client reference on every row of every table that holds work product. Then route reads through a single data-access layer that takes the authenticated subject as an argument and refuses to build a query without one. D1 provides no row-level security policy, so this is application code, and it should live in one file you can hand to somebody to review.
- How should a client be invited to the portal?
- With a single-use token in its own table, carrying the client it grants access to, the role, who issued it, an expiry, and a redemption timestamp. Email the link, accept it once, and record the acceptance. Avoid shared credentials for a company and avoid links that never expire: both end the same way, with an address you have never heard of signing in months later because the original message was forwarded to somebody who left.
- What happens when an engagement ends?
- More than deactivating a login, and it is worth deciding before you need it. Revoke the sessions -- the sessions table carries revocation columns and a reason field precisely for this -- so existing tokens stop working immediately rather than at expiry. End the membership but keep the historical record, since deleting a person erases who approved what. Then answer the question clients ask: does read access to past deliverables continue, and for how long.
- Can clients upload and download documents?
- Yes, with one architectural caveat: the app templates do not bind an object-storage bucket by default, so files are not a database problem you can solve with a column. Use a storage provider, keep the object key, filename, size, uploader, and the client reference in D1, and serve downloads through a short-lived signed URL issued only after the permission check passes. A permanent public link is not access control, however unguessable it looks.
- How should projects and deliverables be modelled?
- Projects belong to a client, milestones belong to a project, and deliverables belong to a milestone with a status of their own. Give every one of them a visibility flag, because the useful portal shows real progress and the honest version of progress includes notes the client should not read. One flag checked in the data-access layer is the difference between a portal your team keeps updated and a second internal tracker that quietly becomes the real one.
- Can clients comment or message through the portal?
- Yes. Attach threads to the thing being discussed -- a deliverable, a milestone, a file -- rather than creating a general inbox, so context is never missing from a reply. Mark internal comments with the same visibility flag the rest of the model uses. Notifications go out from the worker through an email provider, and they should link back to the specific item, because a notification that says there is an update is an invitation to email you instead.
- How do approvals and sign-off get recorded?
- As immutable rows, not as a boolean on the project. Record who approved, at what timestamp, which version of the deliverable, and any comment they left, then leave that row alone forever. A checkbox tells you the current state and nothing about how it was reached, which is exactly the information you need when a client remembers the approval differently. Superseding approvals are new rows; the old ones stay readable.
- Can each client see their own branding?
- Yes, at the level that is worth the effort. Keep a logo URL and a small set of theme values on the client row and read them after sign-in, which covers most of the perceived customisation for almost no complexity. A dedicated domain per client is a platform capability rather than a schema decision, so check what your plan includes at fabricate.build/pricing before you promise one in a proposal.
- How do I know who looked at what?
- By recording it deliberately. The sessions table establishes identity; it does not log intent. If you need to answer whether a client opened a document before the deadline, add an access log with the subject, the resource, the action, and a timestamp, and write to it in the same layer that enforces permission so the two cannot drift apart. Decide the retention period at the same time, or the table becomes the largest one you own.
- Can a client connect the portal to their own systems?
- Through scoped API keys, which the template schema anticipates: a hashed key, a short preview for display, an owner, a scope list, an expiry, and a last-used timestamp. Show the key once at creation and never again, since a key you can display later is one you have stored in a form you should not have. Support rotation from the first version, and keep keys out of logs and error reports.