Video chapters
- AppEngine Customer Self-Service Portal
- Personal identity and company purchasing
- Start from the maintained platform reference
- Read identity before editing
- Save and reopen a profile change
- Use the identifier each record requires
- Choose availability for the saved booking
- Move and verify the appointment
- Preserve the booking when a slot is unavailable
- Guard the cancellation link action
- Cancel and retain the booking history
- Create and revisit a support request
- Verify customer record isolation
- Bootstrap the company and its first manager
- Add a buyer from the manager portal
- Keep the person and purchasing context separate
- Switch context and inspect actual purchasing data
- Apply manager controls and retain a manager
- Accept a company invitation
- Download the portal and multi-language examples
Written guide
Build a customer portal for personal records, appointment changes, support and shared company purchasing. Northside is the sample client built for this lesson; it is not an Appmint administration screen. Dana uses a personal customer account. Priya manages Harbor Logistics, and Noah buys for that company with his own customer identity.
Ready for review — 13 October 2026: the 8-minute video contains 20 chapters at 1440p. This written course includes inspected screenshots and verified response distinctions. Independent sampled picture review, encoded-audio checks and chapter playback checks passed; owner review is pending.
Companion source code on GitHub · Setup and run instructions · Recording-time ZIP snapshot
The maintained companion lives in appmint-examples/appengine-customer-portal. Clone the repository, then follow that folder’s README to build the official SDK, configure your own test customers and run the portal. The public source uses configurable paths and includes the browser UI, trusted local host, JavaScript, TypeScript, Python, curl, JSON and Postman examples. No private recording credentials are included.
git clone https://github.com/JacLight/appmint-examples.git
cd appmint-examples/appengine-customer-portalBefore you start
You need an AppEngine organization and backend address, working customer authentication, and a trusted application server that keeps customer tokens out of the browser. Keep each customer's session separate. Staff setup uses a staff identity with the appropriate business permissions; never put that credential in a customer portal.
Use these existing lessons for the prerequisite implementation:
- AppEngine API Foundations — sign in as the owner: obtain a staff session with customer/shared-account management permissions for company bootstrap. It stays in the staff setup environment; the customer portal does not receive it.
- AppEngine Custom API Gateway: customer sign-in at 01:57, cookie session at 02:35, permitted server routes at 03:21, sign-out at 09:33. Bring the working session and a bounded server-to-AppEngine request adapter.
- AppEngine CRM APIs: configure a bookable service at 04:06, available slots at 04:50, save a booking at 05:34, configured form at 07:07 and submitted answers at 07:49. Bring an existing customer booking with its actual definition, selected service and any required answers.
- Maintained AppEngine API reference: find Customer Account, Reservations and Shared Accounts. This course explains the sequence and consequential choices; use the maintained reference for current full field lists and endpoint examples.
The portal companion supports JavaScript, TypeScript, Python, curl and Postman. Its local host is a labelled teaching client: identity buttons select isolated training customers, then the server signs in with their actual customer credentials. Production sign-in, session storage and deployment are covered by the Gateway prerequisite.
Prepare two distinct personal customer identities, a company customer record, a manager and a buyer. Use only records and recipients you control. Notification dispatch is separate from a saved state change; delivery must be checked through configured providers if you intend to teach delivery.
For customer sign-in/challenges and renewal, use API Foundations §4 and §6. The video pages above provide chapter buttons; the times identify chapters rather than unsupported URL seek fragments.
The twenty sections below follow the current video manuscript. Choose the chapter you need rather than repeating setup for every request.
1. AppEngine Customer Self-Service Portal
Explain. A customer portal joins three things: the person who is signed in, records they are allowed to see, and the business actions they may perform. A useful portal shows a saved result after a change. Changing a date only in the browser is not a completed appointment change.
Walkthrough. Open Dana's actual portal overview, appointment detail and saved support request. Later, open the same interface as Priya and Noah. Keep personal identity visible even when the purchasing context changes to Harbor Logistics.
2. Personal identity and company purchasing
The person and the company are different identities in the model. Dana owns personal records. Noah is a separate customer. Harbor Logistics is a company customer record; Priya and Noah are linked to it through manager and buyer associations. They continue signing in as themselves when buying for the company.
AppEngine supplies customer-oriented routes and business actions. Their boundaries differ: this lesson uses specific own-record routes, manager-authorized shared-account routes and a carefully guarded cancellation action. It does not claim that all routes under a common URL prefix have identical access rules.

Concept illustration, not an app screenshot. Priya manages membership and Noah buys for Harbor Logistics; both retain their own customer identities.
3. Start from the maintained platform reference
Open the maintained AppEngine API reference. Search client-data/profile, select the exact operation and inspect its current request and response. Then find Reservations and Shared Accounts at the steps below.
Keep the API address and organization ID in the trusted server configuration. The customer session supplies the caller identity. The sample uses the official JavaScript client’s supported request and injected-fetch interfaces, with ordinary customer bearer access; it does not create a new SDK or System identity.
The public profile-update description inspected during production still broadly mentions email. Current source and the actual rejected update establish the three-field boundary below; do not copy a stale sentence into your form. Full changing field lists belong in the maintained reference. This lesson explains which operation to choose and what its saved result means.
4. Read identity before editing
Call GET /client-data/profile through the trusted server using the current customer's bearer token and organization header. Use the returned customer record to label the portal. A browser-supplied email is not the authority for selecting a customer's records.
If there is no valid session, return to sign-in. A challenge response is not a finished session: complete the authentication flow from the prerequisite before fetching portal data. Do not render another person's cached profile while the new customer is signing in.
Demonstrate. Select Dana's teaching identity, show the actual authenticated profile result, and open My profile. Sign out and verify that the authenticated view is no longer available. This is a short use of the existing session implementation, not another authentication course.
The current self-service profile patch accepts the flat fields firstName, lastName and phone. Send the changed allowed fields, not the entire record and not a data wrapper. Names and phone values are validated. Email, roles and other identity/access fields are not editable through this operation.
Adding a customer schema field does not expand this self-service allowlist. Email and role changes require their own deliberately authorized path.
5. Save and reopen a profile change
Open My profile, change the phone, and select Save changes. The sample sends a flat allowed-field object, waits for AppEngine, and rereads the profile. Leave and reopen the page to retain that saved value.
The recorded customer now has +13125550199 and phoneVerified: false. This is a saved contact change, not proof of possession or delivery of a verification code.

Actual sample portal connected to AppEngine. The saved phone and verification state are displayed together.
6. Use the identifier each record requires
Use the list response to obtain the identifier required by the corresponding detail operation. Similar-looking route parameters do not guarantee identical contracts.
| Record | List | Detail identifier | Current detail/absence shape |
|---|---|---|---|
| Appointment | GET /client-data/reservations | reservation sk | one record; missing or another customer's record returns404 |
| Order | GET /client-data/orders | order data.number | one record or no body (the client normalizes an empty response) |
| Support ticket | GET /client-data/tickets | ticket data.name | a page whose data array may be empty |
| Form | GET /client-data/forms | normalized returned row | paged rows with status, values and form/schema information |
Demonstrate. Open one actual saved record in each relevant page. Present useful labels: service and local appointment time, order number and status, ticket title and number, form questions and saved answers. Use the form's schema labels where available rather than displaying raw field keys as the finished customer interface.
An empty response is a normal UI state. Do not leave stale detail from the previous record. Current own-order service does not pass all advertised pagination/filter arguments downstream; this sample does not offer controls that pretend those options work.
Forms can be associated by author and stored email fields, including authentication/value fields. The presence of a form row alone is not proof that a verified identity completed it. Show its actual authentication information. In the recorded example, the answers are saved while the returned workflow status remains new: submission and later workflow processing are separate. Do not call the workflow completed simply because its answers were saved, or call those saved answers unsent because the status is new.
In this recording the order is a saved unpaid teaching order. No checkout charge or provider payment is claimed. The submitted form contains its actual goal answer and the form schema, with workflow status new; the saved support request is reopened by ticket number.
7. Choose availability for the saved booking
Start from the saved owned booking. Read its definition, service and current time. Ask for availability for that exact definition/service and pass excludeReservationId so the existing appointment is accounted for during a move. Display the returned local-time choices and exclude or disable entries with no available capacity.
When a time is selected, call the authenticated POST /crm/reservations/reschedule/:id route using the reservation's sk. This current route checks customer ownership. Send the selected start time and a reason; do not use a staff override in the customer portal. The teaching fixture uses notify:false; its saved change is not a delivery demonstration.
The availability response groups slots by day and includes its timezone. A closed day may have no slots; a returned slot can have spotsAvailable: 0. Keep both conditions visible instead of inventing a selectable time. In the companion, availability(sk, from) derives definition/service from the owned record, then move(sk, selectedStartTime) uses the authenticated ID action.
8. Move and verify the appointment
Open Appointments → Change time, choose an actually available result and save. The portal calls the business action, then rereads the same booking.
Recorded booking WWZ00R7M moved to 6 November, 9:45 AM America/Chicago (2026-11-06T15:45:00.000Z). The booking reference remains the same. The saved response, not a local date label, establishes the change.

Actual AppEngine readback shown through the sample portal after the move.
9. Preserve the booking when a slot is unavailable
The controlled destination conflict returned 409, reason: slot_unavailable, and “That time is full (1 of 1 booked).” A fresh owned read still showed booking WWZ00R7M at 15:45Z. Keep that existing appointment and request fresh availability. Do not cancel it, create a duplicate or automatically retry an uncertain write.
Related written check. The profile boundary was also checked: trying to change the sign-in email returned 400; rereading the profile retained its original identity and the already saved phone. Handle these as different platform refusals, not one generic success/error toast.
10. Guard the cancellation link action
The current email-and-booking-code cancellation route is a record-link capability; it does not derive ownership from the bearer token. A signed-in portal must not accept an arbitrary email and code from the browser and forward them.
The supplied trusted server first reads the selected reservation through GET /client-data/reservations/:sk using the current customer session. Only after that owned read succeeds does it derive the saved email and booking code, call the cancellation action and read the reservation again. The browser supplies the selected reservation ID, not someone else's identity fields.
The current DELETE /client-data/reservations/:id service wiring is not the verified cancellation path for this lesson. Use the guarded adapter shown here until its contract is corrected and reverified.
The server accepts only the selected booking ID from the browser. If its customer-owned read fails, no cancellation call is made. This sequence is in Portal.cancel and is covered by an offline refusal-path test; direct email/code API authorization is not changed by the sample.
11. Cancel and retain the booking history
Confirm Cancel appointment, then reread and reopen the same booking. The expected completed outcome is a retained record with status: cancelled, rather than deletion from history. Keep its reference and date visible so the customer can identify what changed.
Notification delivery is a separate provider outcome. This lesson uses an isolated organization with outbound delivery disabled; it does not claim that an email or text arrived. The completed capture now shows booking WWZ00R7M reopened with cancelled status and its retained 9:45 AM appointment details.

Actual sample portal after cancellation and saved-state reload. The booking reference and customer remain visible.
12. Create and revisit a support request
Use POST /client-data/tickets with the request's title and description in its expected data object. AppEngine uses the customer identity as reporter and applies its ticket defaults. Keep credentials and reporter selection out of customer-editable fields.
Demonstrate. Enter a concise support question, save it, obtain its ticket number and open it through the detail route. Reload the page and show the saved title, description and current status. The number comes from the saved result; do not fabricate it or treat a generic toast as proof.
Attachments use the separate multipart ticket operation. File upload configuration and file permissions are covered by the File Storage lesson; link it when offering that extension rather than pretending a text-only request demonstrates attachments.

Saved teaching ticket S2RW5LX5, its title, description and current status. This is customer request persistence, not a delivered reply.
13. Verify customer record isolation
Open Noah's independent session. Dana's records must not appear in his personal list. Test Dana's exact selected identifiers through the same trusted portal routes, then inspect each actual outcome: booking404, absent order with no returned record (observed HTTP200 empty body), absent ticket as an empty result page.
Normalize these into clear customer messages such as “Appointment unavailable” or “No matching order in this account.” Do not require every route to return404. The check concerns data returned and permitted actions, not whether the browser hides a button.
Show. The second customer's identity remains visible during each attempt. Never overlay Dana's record under Noah's name as a misleading illustration of a successful access check.

Concept illustration, not UI proof. The specific observed refusal shapes are described above; this does not imply every AppEngine route has the same access contract.
14. Bootstrap the company and its first manager
Harbor Logistics is represented by a company customer record. Memberships link individual customer emails to that company with manager or buyer role and active/suspended status. Each person still signs in with their own account.
Create or select the company customer through the customer API or the Customers interface. In this isolated exercise, POST /profile/customer/signup creates a company contact using its controlled company email, name and company fields; it is not a direct Repository customer write. Keep the returned company sk. Then staff calls POST /crm/shared-accounts/:accountId/members with that ID and the existing manager customer email plus role: "manager". Staff creates the company's first manager through this shared-account staff operation. The portal manager then uses the customer-authorized shared-account operations. This separates organization administration from the day-to-day company portal.
The company signup response nests the customer under customer; retain response.customer.sk, not a nonexistent top-level response.sk. Add the first manager with the authorized staff session, then leave staff credentials behind when using Priya’s customer portal.
15. Add a buyer from the manager portal
Sign in as Priya and open Company account → Add a member. Select Noah’s existing customer email and Buyer. AppEngine creates or reinstates his membership immediately; reload the list to read its role and status. Store each row’s associationId for later updates.
Adding an existing customer can initiate an account-added notification. It skips invitation acceptance, not notification handling. The recording verifies the saved association with outbound delivery disabled.
16. Keep the person and purchasing context separate
Keep Noah as the signed-in person while he chooses Harbor Logistics. POST /profile/customer/shared-account checks his active association and returns replacement customer and refresh tokens. Store both on the trusted server before the next request. Sending {} returns to personal context.
GET /storefront/cart/mine reads a saved cart for the current person and selected company context, without creating a cart. The legacy cart/get route can auto-create and is a different operation. Selecting an account restores a context; it does not create product lines. A company selection also does not turn a buyer into a manager.

Concept illustration, not an app screenshot. Selection replaces the session context and reads the previously saved company cart; it does not add products or take payment.
17. Switch context and inspect actual purchasing data
The owned fixture now has two distinct saved carts: Noah’s personal cart contains one PORTAL-GUIDE-1013; his Harbor Logistics cart contains two, with the company’s sharedAccountId. Select the company, reread the saved cart, then return personal and reread the other cart. Compare the cart IDs and quantities as well as the heading.
Noah’s member-management request returned 403. Priya’s manager-only company-orders route returned the saved company order. These are separate permissions from selecting a purchasing account. The orders are unpaid teaching records; no charge is claimed. The replacement recording shows the company cart with two items, then returns to the separate personal cart with one.

End of the recorded company-to-personal switch: Noah remains the person, the current context is Personal, and the saved cart contains one item. The company phase uses a different saved cart with two items.
18. Apply manager controls and retain a manager
Use the buyer's session to request management data and show the actual refusal. Use Priya's session to suspend Noah, then attempt a fresh company selection as Noah and show refusal. Reinstate him and verify that a fresh selection succeeds.
Try to demote or suspend the company's last active manager through the customer management operation. Show the refusal and the retained manager. These are useful recovery boundaries, not instructions to change unrelated application code.
Do not claim immediate revocation of an already-issued token's selected-company context. The current selection endpoint checks fresh membership, while the request middleware can retain selected context from an existing token. Applications requiring immediate revocation need that boundary addressed and independently verified; changing a member's row alone does not demonstrate it.
Actual controlled results: last-manager suspension returned 400 (“The account must keep at least one manager”); suspended Noah’s fresh company selection returned 403; after reinstatement a fresh selection returned 201. These outcomes establish the selection and management checks, not immediate invalidation of every previously issued token.

Actual saved membership list: Priya remains manager and Noah is active again. The separate recorded refusal precedes this final state.
19. Accept a company invitation
A genuinely new email creates a pending invitation instead of immediate existing-customer membership. Keep pending and active states distinct. The invitation lifecycle includes expiry, validation, matching signed-in email and acceptance. An already accepted or expired invitation is not a fresh membership grant.
Re-inviting an existing suspended member can reinstate the same association; it does not have to create a duplicate row. Re-inviting a pending address can refresh the invitation. Show the actual returned state and reload the member/pending list.
For the controlled demonstration, use an owned training address and a captured invitation token kept off-screen. If delivery is disabled, explain the controlled link handoff rather than claiming that an email arrived. Production delivery requires the configured provider and its own delivery evidence.
The recording’s new recipient, Elli, was invited before her customer account was created. After she signed in with the matching email, the sample showed her pending invitation, validated it and accepted it. Her company membership then appeared. The token stayed on the trusted server: the browser used a session-specific opaque invite handle, refreshed against the current customer profile on each action.
After acceptance, refresh the profile; selecting the company is still a separate step that replaces the purchasing-context session. Treat a pending link as a pending invitation rather than an active member.

Actual accepted invitation in the sample portal. This used a controlled training handoff and is not delivered-email evidence.
20. Download the portal and multi-language examples
Return to the actual portal: Dana's corrected profile, moved then cancelled appointment, saved support request, and Priya's company membership view. Show that Noah's personal identity and purchasing context remain separate.
The completed implementation is a client of AppEngine's customer and business contracts. Keep the maintained reference and linked prerequisite courses beside the runnable examples. Extend the portal with additional modules when their own authorization, configuration and saved outcomes are understood; do not substitute direct record writes for the business action simply because the UI looks similar.
Open the maintained companion on GitHub and follow its README for JavaScript/TypeScript, Python, curl, JSON and Postman. The package includes the actual sample page and trusted-server adapter; private training credentials and recorded customer sessions are excluded. Eleven JavaScript and four Python offline tests passed; Postman’s local19-request structural lint passed. These checks supplement the recorded AppEngine outcomes, not replace them.
Additional portal modules deserve their own verified scope. Named wishlists use a wishlist ID plus product SKU; address removal in this sample performs a trusted owned-list check. Saved-payment-method writes currently return501 and the list is a stub, so this lesson does not show a working card vault. Attachments, messaging, benefits and delivery require their corresponding configured feature lessons.