Customer portals

Everything else in hdls is the internal side: you and your team operating your backend. Customer portals add a second audience — your own customers — who can interact with a narrow, principal-scoped slice of the same backend. The reference case is support: let your customers raise and track their own tickets directly, without ever seeing anyone else's.

💬 Just ask

  • "Set up a portal so my customers can raise and track their own support tickets."
  • "Make sure each customer only ever sees their own tickets."

You don't call these tools yourself — just tell your assistant; the technical reference below is for when you want the details.

This is dual-persona: the same workspace, the same data, two audiences.

  • Agent persona (the default) — your internal operators. Whole-workspace visibility, subject only to the role ladder. This is every credential you already use.
  • Customer persona — an external requester. Sees only their own rows, never another customer's, enforced by the database itself.

Available for support today. The dual-persona model is built and shipping for the support product: a customer can create a ticket, list and read their own tickets, and reply on a ticket they own. The same mechanism generalizes to other two-sided products over time.

At a glance

Customer toolWhat it does
create_ticketOpen a new ticket as the signed-in customer (their id is stamped automatically)
list_my_ticketsList only the customer's own tickets
get_ticketFetch one of the customer's own tickets by id
replyPost a reply on a ticket the customer owns

A customer credential gets exactly these named, narrow tools — and nothing else. No generic queries, no writes outside their own rows, no analytics, no schema access.


Per-principal isolation — A never sees B

A customer credential is bound to a single principal (e.g. one support requester). Two layers keep customers apart:

  1. Database-enforced row filtering. A restrictive row-level-security policy scopes every read and write to the customer's own principal. A customer bound to requester A can only ever see rows where the requester is AB's tickets and replies are invisible at the database level, not by application code that has to remember to filter.
  2. Server-stamped identity. When a customer creates a ticket or replies, hdls sets their principal id itself, from the credential — the client cannot supply someone else's id, and the database rejects a forged one anyway.

The result is strict isolation within your one workspace: customer A and customer B share the same backend, the same schema, the same product — and still can't see each other's data.

Fail-closed. A customer credential with no principal sees zero rows, never the whole workspace. Misconfiguration errs toward showing nothing, exactly like workspace isolation does.


What a customer can and cannot do

The customer surface is an allowlist, deliberately tiny:

A customer can:

  • Create a record they own (create_ticket) — their id is stamped automatically.
  • List their own records (list_my_tickets) — only theirs are returned.
  • Read one of their own records (get_ticket) — another customer's id returns nothing.
  • Reply on a record they own (reply) — replying to anyone else's is rejected because the parent isn't visible to them.

A customer cannot:

  • Run generic query_records / insert_record / update_record / delete_record.
  • See schema shape (list_tables / describe_table).
  • Run aggregates or analytics (counting other people's rows is itself an info-leak, so these aren't offered).
  • Service tickets the way your agents do — no assign, resolve, or list-all.

A customer is always low-capability, regardless of any role. The narrow tool set is the attack-surface layer; the database row-filter is the security backstop. Both ship together.


How identity is bound

A credential becomes a customer credential at issue time, never at request time. There is no header, claim, or argument a client can send to become a customer or to change which principal it is — both are frozen into the credential by whoever issued it. A customer signs in (verified identity), and hdls binds their credential to the matching requester row in your workspace. From then on, that binding is immutable for the credential's lifetime.

This is what makes it safe to hand an MCP endpoint to people outside your team: the worst a customer credential can ever do is act as that one customer, on their own rows.


A typical customer flow

  1. Your customer connects with their customer credential (bound to their requester identity in your workspace).
  2. They open a ticket:
    // tool: create_ticket
    { "values": { "subject": "Login broken on mobile", "body": "Can't sign in since the update." } }
    
    Their requester id is stamped server-side — they can't open a ticket on anyone else's behalf.
  3. They check on it later:
    // tool: list_my_tickets
    {}
    
    Only their tickets come back.
  4. They add a reply:
    // tool: reply
    { "ticket_id": "tkt_01H…", "body": "Still happening on the latest build." }
    
    Replying to a ticket they don't own is rejected.

Meanwhile your internal agents work the same tickets from the agent side — full visibility across the workspace — using your normal credentials. Same data, two personas, cleanly separated.

Where to go next