@awesomate/hosting-mcp
Version:
Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, n8n automations, and build Node/static apps + databases
221 lines (176 loc) • 13.5 kB
Markdown
---
name: awesomate-portals
description: Build a portal or members' app on the account's own data with @awesomate/sdk. Customers sign in by email link and see only their own records, staff see everything, lists update live, and optionally an AI assistant answers in conversations. Use when the user says "customer portal", "client login", "members' area", "let my customers see their jobs / bookings / orders", "staff dashboard", "chat with customers", "portal", or wants people outside the business to sign in to something of theirs. Companion to awesomate-app-builder (hosting the page), awesomate-database and awesomate-n8n: same connection, same token.
---
# Awesomate Portals: people sign in and see their own records
A portal is three things the account already has, put together: **kinds** (the app's own tables,
in the account's own database), **access rules** on each kind (who sees and changes what, enforced
by the database itself), and an **app** (where it runs, how people sign in). The page uses
`@awesomate/sdk` straight from the browser. There is no app server to write.
**The SDK's docs:** https://hub.awesomate.ai/docs/sdk/ (a tutorial that builds exactly this kind of
portal, a guide per feature, and a reference generated from the SDK's code). When you are unsure
of a method or an option, read https://hub.awesomate.ai/docs/sdk/llms-full.txt, the whole site as
one text file, rather than guessing.
Work in plain words with the owner. They describe their business ("customers request jobs, we
quote, they see the status"), you turn that into kinds and rules, show them the design in a
sentence or two, and only then create anything. Kinds, rules and apps are part of their data,
not scratch space.
## 0. First, every time
1. `awesomate_whoami`: everything below acts on that account. Fix the connection if the slug is
not the one the user means.
2. `awesomate_get_context`: check `features.contacts`. `available: false` means stop and say its
`message` in one line; never offer portal work then (no `features` block or no entry: carry
on as before, never read that as unavailable). Then check the plan. **Sign-in for an
app's people is Pro and above.**
Creating kinds and writing data is Support Plus and above. Reading is every plan. A 403
carries the upgrade path: relay it honestly, never retry it.
## 1. Understand who signs in and what they do
Ask, one or two at a time:
- Who signs in? Usually **customers** (role `member`) and **staff** (role `staff`).
- What does a customer see? ("their own jobs, and the messages on them")
- What can a customer do? ("request a job, send a message")
- What do staff do? ("see every job, set status and quote, reply")
- Who may sign up: only people the owner adds (`invite`, the default and the safe choice), or
anyone with the link (`open`, everyone gets the default role)?
## 2. Design the kinds
`awesomate_crm_kinds {action:'define'}`. Typical portal:
| Kind | Attributes | Links |
|---|---|---|
| `job` | `title` (text, required), `status` (choice), `quote` (money) | `customer` to `contact` |
| `message` | `body` (long_text, required) | `job` to `job` (required) |
- **Link the thing to the customer's contact.** That link is what "their own" means.
- Mark an attribute `readable_by_ai` only when the owner wants Claude and AI assistants to see
it. Never for anything sensitive. `readable_by_ai` is only about AI.
- **Fields only the team sees** (a cost, an internal note): give the attribute
`visible_to: ['staff']` when defining it, or later with `awesomate_crm_kinds
{action:'set_visibility'}`. Customers read it as null and cannot write it; the database
enforces it, and so does what the assistant sees. Without `visible_to`, everyone who can read
the record sees the attribute. Don't make such an attribute `required` on a kind customers
create, or they cannot create one.
- Show the owner the design ("A job has a title, a status and a quote, and belongs to a
customer. Messages hang off a job.") and get a yes first.
## 3. Set the access rules
`awesomate_crm_kinds {action:'set_access'}`. Rules are per kind and per role:
`all`, `none`, `own` (records they created), or `linked:<path ending at a contact>`.
```json
job: {"read": {"member": "linked:customer", "staff": "all"}, "write": {"member": "linked:customer", "staff": "all"}}
message: {"read": {"member": "linked:job.customer", "staff": "all"}, "write": {"member": "linked:job.customer", "staff": "all"}}
```
- A role with no rule gets nothing. The database applies these to every read and every write, so
a mistake in the page can never show anyone more than their role allows.
- A person can only link a record to something they can see: a customer can point a job at their
own contact, never someone else's.
- **Staff putting a job under a customer** needs one more switch, per app: app people read no
contacts but their own, so by default staff can only link to themselves. Ask the owner, then
`awesomate_crm_apps {action:'customer_lookup', app_id, roles:['staff']}`. Those roles can then
search customers (name, email, phone; never a sensitive field) and link a record to any of
them. Never the app's default role (the one customers sign in with); the hub refuses it.
## 4. Create the app and add people
1. `awesomate_crm_apps {action:'create', name, allowed_origins, sign_up}`. `allowed_origins` are
the exact addresses the page runs on (`https://portal.example.com.au`, plus
`http://localhost:5173` while building). The name appears in the sign-in email. The answer
has the **publishable key** (`pk_...`): it is not a secret and goes in the page.
2. `awesomate_crm_app_users {action:'add', email, role}` for each person. A customer is matched to
their contact by email. Staff get `role: 'staff'`. No email is sent; they ask for a link on the
page.
### Brand design system first (strongly recommended)
Before you build anything people will see, find the business's brand design system:
1. `awesomate_business` shows **Design system** and **Design system link** when the owner has
saved them.
2. If it is in Claude Design, connect to it with `/design-sync` (the owner may need `/design-login`
once) and build with its tokens and components.
3. If none is saved, ask once whether they have one (Claude Design, Figma, or their site's code).
4. If they don't, offer to make one: `awesomate_design_system_package` writes a starter design
system from their confirmed details into the project, and `/design-sync` puts it in Claude Design.
5. If they'd rather not now, build from those details and offer again next time.
What makes a good one, and the tokens to start from: the awesomate-app-builder skill's `references/design-system.md`.
## 5. Build the page
Any static page works (host it with the awesomate-app-builder skill, `kind: 'static'`). The SDK
loads from a CDN, or from npm in a Vite app:
```html
<script type="module">
import { createAppClient, AwesomateError } from 'https://cdn.jsdelivr.net/npm/@awesomate/sdk@0.18/+esm';
const app = createAppClient({ publishableKey: 'pk_...' });
// The email link signs them in by itself (also when it opens in a tab already on this page).
app.auth.onChange((user) => (user ? showPortal(user) : showSignIn()));
app.auth.onSignInError((err) => showMessage(err.message));
const user = await app.auth.user();
user ? showPortal(user) : showSignIn();
// Sign-in form
await app.auth.signInWithLink(email, { redirectTo: location.href });
// Data: the same calls for everyone; the rules decide what each person gets.
const stop = app.live('job', { orderBy: ['created_at', 'desc'], limit: 100 }, (rows) => renderJobs(rows));
await app.write('job', { title: 'Leaking tap', status: 'open' }, { links: { customer: user.contact_id } });
await app.write('message', { body }, { links: { job: jobId } });
// Staff, once customer_lookup is on for their role: find a customer, file a job for them.
const [customer] = await app.lookupCustomers({ q: 'jo' });
await app.write('job', { title: 'Gutters', status: 'open' }, { links: { customer: customer.id } });
</script>
```
- Choose the view from `user.role` (`staff` sees everything and edit controls; `member` sees
their own records and a request form).
- `live()` keeps a list current: anything changed anywhere (staff, Claude, n8n) shows at once.
- To test customer and staff side by side in one browser, give the staff view its own
`storageKey` (for example when the address has `?staff`).
- Serve the HTML with `Cache-Control: no-cache` (an `.htaccess` rule on Awesomate hosting), or
people keep an old copy of the page after you change it.
- **Never put the account's token (`amt_pat_...`) or a server key (`ak_...`) in a page.** Only
the `pk_` key belongs in the browser.
## 6. Optional: an AI in the conversations
`awesomate_crm_assistant` adds an assistant (for example "Bree") to the message threads. Each
conversation is Off (people only, the default), Draft (a suggested reply only staff see) or Auto
(she replies, marked as AI, when she is sure). She reads as the customer she is answering, so
the access rules bound her, and only `readable_by_ai` attributes reach her. The tool description
lists the two staff-only kinds it needs (`assistant_thread`, `assistant_draft`) and the
`from_assistant` attribute on the message kind. Give her a published, public, strict Knowledge
agent (awesomate-knowledge skill) so she can answer questions about the business. Confirm the
name and the default mode with the owner: switching it on lets an AI reply to their customers.
## 6a. Optional: things customers may do that their rule does not allow
Accepting a quote is the usual one: customers can't edit a job, but they should be able to accept
its quote. Save a write recipe (`awesomate_crm_recipes {action:'save'}`) whose steps do exactly
that, then open it to their role (`{action:'run_by', key, roles:['member']}`). The page runs it
with `await app.call('accept_quote', { job: jobId })`. The database checks every step against
the recipe as saved, and the job must be one they can read. Guide:
https://hub.awesomate.ai/docs/sdk/guides/customer-actions/
## 6b. Optional: who's here, and talking by voice
- `const room = app.here(jobId, (people) => render(people))` shows who else has the job open and
who is typing (`room.typing(true)` on input, `room.leave()` on close). Guide:
https://hub.awesomate.ai/docs/sdk/guides/who-is-here/
- Voice: pick one of the account's agents for the app (`awesomate_crm_apps {action:'update',
app_id, voice_agent_id}`; the agent needs In your app switched on and the page's address under
its "Where your app runs"). The page loads the hub's widget and mounts it with
`agentId: await app.voiceAgent()` and `session: () => app.voiceSession()`. Guide:
https://hub.awesomate.ai/docs/sdk/guides/voice/
## 6b2. Optional: file uploads
People signed in to the app can attach files (a photo of a job, a signed form) when the owner
switches it on: `awesomate_crm_apps {action:'update', app_id, file_uploads:'private'}` ('public'
also gives each file a public address; web pages and scripts are refused there). Off by default;
with open sign-up anyone could upload, so ask the owner. Give the kind a `file` attribute
(`awesomate_crm_kinds {action:'add_attribute'}`), then the page does
`const f = await app.files.upload(file)` (10 MB each) and stores `f.handle` in that attribute;
`app.files.download(handle)` gives the file back to anyone in the app who can read the record.
Uploads count toward the account's Files space. Guide: https://hub.awesomate.ai/docs/sdk/guides/files/
## 6c. Optional: reply emails
`awesomate_crm_notifications {action:'set', app_id, notifications: {enabled: true, thread_link: 'job'}}`
emails the other side of a conversation when someone writes there and they are not in the app:
the customer when the team or the assistant replies, the team when a customer writes. At most one
an hour per conversation, from the app's name, with the message quoted and a link back. They are
system emails (no unsubscribe link); the owner switches them on or off per app. Ask the owner
first: it emails their customers.
## 7. Optional: automation
- **Server key** (`awesomate_crm_apps {action:'create_key'}`): for the app's own server or an n8n
workflow to read and write records. Read or write, only the kinds it needs. For n8n pass
`n8n_credential: true`: the hub puts the key into a credential on their n8n and you never see
it. Otherwise it is shown once: straight into the server's environment, never a reply or a repo.
- **Record events** (`awesomate_crm_hooks`): when a record of a kind is created, updated or
archived, the account's own n8n gets an event at once. Build and activate the workflow first
(awesomate-n8n skill), then create the hook with its production webhook URL and
`n8n_credential: true`, and pick the credential it names in the Webhook node (Header Auth).
Use `test` to check it. Example: a job is
requested, the workflow drafts a quote and writes it back with a write key for `job`.
## 8. Check it works
- Sign in as a customer and as staff (two windows). The customer must not see another
customer's records; try it.
- Write as one, watch the other update live.
- With an assistant: set one job to Draft, ask a question as the customer, see the draft as staff.
- Tell the owner what you built in their terms, and what is not built yet.