Every task, comment, message, and audit event in Workhouse carries an internal or client flag. The portal's queries filter on it. The dashboard ignores it. One database, two audiences, zero leakage.
Pick any PM tool — Notion, ClickUp, Asana, Trello. The agency-shaped problem shows up on day one. You need internal-only conversations and client-visible deliverables to coexist on the same project. The tools force a choice: invite the client to your whole workspace (and watch what you say), or maintain a parallel client-facing workspace (and sync it forever).
Both choices fail the same way. The internal/external boundary lives in your head, in the slack channel where you discuss the client comment, in the carefully renamed Trello column. Sooner or later, something slips.
One flag, applied everywhere it matters.
When you create a task, you set it Internal or Client. Default is Internal. Flip it Client when you're ready for the client to see it.
A comment on a Client-visible task defaults to client-visible. If you need to discuss something internally on the same task, flag the comment Internal. Both threads live on the same task; clients see one of them.
Every portal query has `WHERE visibility = 'client'` in its where clause, plus the client scope. The portal cannot load internal rows. A guessed URL returns a 404, not a 403.
For agency operators who've been burned by “client portals” that hide things in the UI — here's where the boundary actually is.
-- Portal task query (simplified)
select * from tasks
where client_id = $1 -- scope to this client
and visibility = 'client' -- never load internal rows
The visibility column has a CHECK constraint — internal or client, no other values allowed. The portal's session can't escalate to internal scope. There's no UI toggle that flips it; the column is read-only from the client side.
Most “client portals” in PM tools render a filtered view of the same data the rest of the workspace sees. Hide a column in the UI, drop a key from a JSON response, call it a day. That works until someone reads the request log or guesses an ID. Workhouse handles visibility one layer down — what the application doesn't load, no UI bug can leak.
Visibility isn't just on tasks. It's on every entity where the boundary could matter.
Internal-default. Flip to client when ready. Tasks themselves are the most-used surface for the flag.
Inherit the parent task's visibility but can be overridden per-comment. Lets you have an internal thread on a client-visible task.
Per-client message threads have visibility per-message. Useful for back-channel context without surfacing to the client.
Every status change, assignment, approval gets a visibility flag inherited from its parent. Clients see the events that affect their view.
Always internal. The audit log is the immutable internal record — never client-visible by design.
Always client-visible (they exist to assign to clients). Action items are the explicit cross-surface contract.
Beyond the binary internal/client flag, each team can configure which optional task fields (priority, due date, assignee, project name) appear in client-visible task views by default. Some agencies want the client to see who's working on what; others deliberately abstract that.
Title and status always render on a client-visible task — they're load-bearing for the portal UX. Everything else is a team setting, with per-task overrides when needed.
Internal. The agency works on it first; flip to Client when it's ready to share. This default is reversible — teams can set Client-default if their workflow is more client-facing-by-default, but most agencies leave it at Internal.
No. From the client's perspective, the task appears when it becomes client-visible — they don't see prior history. If you want the client to see the work-in-progress thread, you can copy the relevant comments to the client-visible side; the original Internal thread stays where it was.
Yes. Mass-action on a list of tasks updates visibility for all selected. Useful when wrapping up a phase and exposing the whole batch at once.
Attachments inherit the visibility of the task they're attached to. An Internal task with attachments doesn't expose those attachments to the portal — same SQL filter.
In a sense, yes. If you flag every task Client-visible by default, the model effectively becomes a no-op — your client sees everything. Some agencies operate that way for very transparent engagements. The model is still there in the data; you just stopped using it as a filter.
See the model in context: client portal → · audit log → · vs Notion's workspace model →