UNPKG

@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
--- 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.