docs
/
Courses — AppEngine

AppEngine: Custom Web and Mobile Client Development

Watch on YouTube ↗

Video chapters
  1. Custom Web and Mobile Client Development
  2. One organization, one booking, two clients
  3. Endpoint, organization and customer prerequisites
  4. The platform booking sequence
  5. Find the current API contract
  6. Definition, services and booking policy
  7. Customer sign-in and verification outcomes
  8. Official Flutter client and session storage
  9. Select the definition, service and slot
  10. Configured schema becomes customer controls
  11. Required answers before booking
  12. Construct the booking from selected values
  13. Create and read back saved answers
  14. Client validation and trusted enforcement
  15. The same booking on native iOS
  16. Unavailable times and unknown create outcomes
  17. Reschedule while preserving record identity
  18. Cancellation capability and owned-detail check
  19. Change and cancel through the mobile client
  20. Check the second customer boundary
  21. Refresh, sign out and the next visit
  22. Run and extend the connected client

Written guide

Written review status: revised against current source on 11 October 2026; the service-booking implementation sequence and its tested boundaries are covered below. Video status: platform-coverage revision remains in progress. The existing 10:51 review cut demonstrates customer sessions and browser/native booking, but omits dynamic definition/service selection and configured intake forms. Its technical QA passed; complete course coverage did not. The written revision below takes precedence. The approved replacement narration and revised scene renders are complete. The 22-chapter replacement has been exported and is undergoing independent audiovisual review; the older 10:51 video is not the complete revised course.

Course catalogue · Runnable source and contracts

What this course builds

A customer signs in to an organization, chooses a booking type, service and available time, completes the configured intake form, creates a reservation, returns to the same saved record on another device, then changes or cancels it. The web and native applications consume the same AppEngine customer and reservation APIs. Their design is replaceable; the identity, business hours, timezone, capacity, ownership and persisted results belong to AppEngine.

This is an AppEngine implementation course. Flutter supplies the reference UI and the official customer-authentication client. It does not replace instruction about the platform's organization, identities, definitions and API contracts.

Obtain the platform details first

Complete API Foundations: organization and addresses, 00:50. Retain your organization ID and assigned AppEngine HTTPS endpoint. Use the actual endpoint for your deployment; the public Studio address and the API base serve different purposes. The same course's customer signup, 01:51, sign-in challenge, 02:26, verification, 03:02, and refresh, 05:53 show the identity operations needed here.

For staff setup, complete API Foundations: sign in as the owner. Finish any staff verification challenge or required password change before retaining the resulting access token. Use that staff token only for configuration requests; the customer session used later is separate.

Have one organization's configuration-administrator account for service setup and two separate customer accounts for the learner and isolation check. Customer signup is not platform-organization creation and does not require a root administrator. Do not ship the configuration administrator's credentials in a browser or mobile application.

Configure the booking service

The training definition Juniper design consultations is active, has weekday opening hours 09:00–17:00, timezone America/Los_Angeles and capacity 1. It offers Room planning (30 minutes) and Whole-home consultation (60 minutes), both price 0 with a 15-minute buffer. Its Form setting points to the cc_intake_1011 collection. Keep the definition's returned sk and the selected service's exact name; they are different identifiers. Opening hours without the correct timezone do not define the same booking window.

Create the intake collection first, then the definition that references it. Both staff setup requests use POST /repository/create; their datatypes are collection and reservation_definition respectively. The downloadable intake collection defines its fields and rules. The definition setup supplies both services, hours, timezone, capacity and the Form reference. Replace names deliberately in your organization and keep the reference consistent. In Studio, open Reservations → Reservation Definitions and the chosen definition's full settings to inspect services and Form; the corresponding schema is in Database. The custom client consumes that configuration. This course uses a zero-price service to focus on connected customer clients. Paid checkout and payment-provider integration belong to the Headless Storefront course; do not present a free reservation as a demonstrated payment.

Follow setup through its saved result:

  1. Sign in with the staff setup identity and send the intake-collection body. Retain its data.name; this is the Form reference. Its record sk is not the Form value.
  2. Send the definition body with that exact name in data.form. Keep the returned definition sk for DEFINITION_ID.
  3. Read definitions and select that sk. Confirm both service names/durations, workdays, hours, zone, capacity and Form reference survived saving.
  4. Sign in as the customer and resolve the named collection schema. Confirm its required array and three properties before rendering inputs. A staff-only read is not proof that the customer client can load it.
  5. Choose a future working day and request slots for each service. Compare their returned endpoints and spacing, then continue with Room planning.

A notification queue entry is not evidence of an email delivered. Outgoing mail is disabled in this isolated course organization. A deployed application must configure and independently test its chosen delivery provider.

Concept: organization service, customer session, availability and saved reservation

Concept illustration; the following captures are actual application screens.

Register and sign in as a customer

Send orgid with requests. Customer signup uses POST /profile/customer/signup with email, password, firstName and lastName. Signin uses POST /profile/customer/signin. A response can contain a session or a verification challenge. Only a completed session permits the customer's owned reads; a challenge response is not a token.

The reference application reuses the official appmint_flutter_client authentication layer. SignedIn, NeedsVerification and SignInRejected are distinct results. The verification screen completes the official challenge object; the error screen preserves refusal. Tokens and refresh tokens never belong in screenshots, source examples or log messages.

Connect the official Flutter package

Use the official appmint-client/appmint_flutter_client package. The public AppmintAuth constructor accepts its transport and session-store interfaces. This deployment accepts ordinary customer Bearer authorization; the sample's CustomerTransport supplies that route while preserving official auth/challenge/refresh handling. It avoids the default transport's separate app-key bootstrap. No System identity is needed for these customer operations.

The browser demonstration stores tokens in memory, so reload means signin. Native uses the sample's CustomerSessionStore, implementing the official interface with OS secure storage for current and remembered sessions. The host adapter returns mutable remembered-session lists because authentication updates that list. SDK source remains unchanged. The Authentication customer-session lab demonstrates a server-held token and an HttpOnly browser cookie. This is an alternative access arrangement; it is not equivalent to browser memory.

Follow the complete local-run instructions. Public compile-time settings are endpoint, organization, definition ID and practice day. Enter customer credentials at runtime. Do not put credentials in distributable build defines.

For a separately hosted browser, permit the frontend origin at the AppEngine deployment. The current self-hosted production setting is ALLOWED_ORIGINS (comma-separated origins). A browser CORS refusal is distinct from a failed customer signin; native HTTP does not use browser CORS. The reference web build was tested against the permitted local training origin, with no local address displayed in the lesson.

Find the current API contract before building the flow

Open the maintained AppEngine API Reference. Search for reservations and open GET /crm/reservations/definitions. Read the authorization/header section, the response schema and its example. Follow the selected definition into the slots operation and then the customer reservation operation. Open the schema section for the configured collection when the definition names a form. The point is to learn where the current contract lives, then apply it; this course does not ask you to memorize a frozen JSON listing.

Current AppEngine reference: find the definition operation

Captured from the current local reference implementation, without sending credentials or changing records; this is not a claim that the hosted deployment was reverified.

Useful exact reference links:

OperationCurrent reference
Discover bookable definitionsGET definitions
Ask for one service's timesPOST slots
Resolve the configured collection schemaGET collections
Submit the customer's bookingPOST customer reservations
Create the setup collection/definition as staffPOST repository create
Read a customer’s saved bookingGET reservation detail
Read the customer’s bookingsGET customer reservations
Load a range for reschedulingPOST availability
Move the owned reservationPOST reschedule
Cancel by email and booking referenceDELETE cancellation

Reference example correction (14 October): the inspected documentation still showed older servicePointId/startDate/endDate examples for booking and slots. The demonstrated, source-verified flow uses reservationDefinitionId + serviceName + serviceDate for slots, then reservationDefinitionId + service + returned startTime/endTime + configured form answers for creation. The narrow documentation correction is applied to the local source and shown in the labelled reference preview. Hosted deployment has not been verified. Slot length and buffer come from the selected service; the request does not override them with an arbitrary duration.

The HTML reference is the navigation surface; endpoint links expose the maintained detail directly. Use the endpoint assigned to your deployment if its installed version differs from the hosted reference. A documented route is not a substitute for opening the current reference before integration.

Discover definitions and let the customer choose a service

A reservation definition is the organization's booking policy. Its sk is the identity used in subsequent requests. Its title is a display label. Its services have their own names, duration, price and optional recovery time (breakAfter). Do not use a display title as the record ID or quietly substitute the first service.

Read the definitions endpoint. The current service returns active definitions, enriched for the organization. Active discovery is not a guarantee that every returned definition can take a booking: additional bookable/type rules can still make the slots response unavailable. Do not silently convert a table, event or host-dependent definition into this timed-service UI. Render a booking-type picker from the returned records and a service picker from the chosen definition's data.services. Show duration and price before time selection. When the definition changes, discard its old service, host, slot and form answers, load the new form, then fetch new availability. When the service changes within that definition, clear the selected time and query again because its duration and recovery time can change which slots fit.

Actual native booking-type and service selection

The extended practice definition Juniper design consultations offers Room planning (30 minutes,15-minute buffer) and Whole-home consultation (60 minutes,15-minute buffer). Both are free in this example. Select each and compare its returned times. A paid service needs the payment flow taught in Headless Storefront; a numeric price by itself is not proof of a completed charge. When services is absent, the current fallback service uses data.name and data.duration (or 30 minutes). An explicitly empty services array is a different configuration and must be repaired or rejected, not assumed to be the same fallback. The provided fixture has two named services. Inspect the returned contract before extending the client to other definition types.

Read availability

Read GET /crm/reservations/definitions and choose the intended definition. Call POST /crm/reservations/slots with reservationDefinitionId, serviceName and serviceDate (YYYY-MM-DD). Retain exact returned start/end instants and business timezone. Display using timezone data, not a hardcoded UTC offset or the device timezone. A visitor travelling overseas must still book the business's intended local hour.

An empty day is a valid outcome. Offer another date. The closed-day response can contain slots: [], closed: true and an explanation in error; the one-day request can succeed at HTTP level while offering no times. Do not treat that explanation as a customer-authentication failure. A slot result is availability at the moment it was read, not a held place. The create operation rechecks capacity.

Understand the choices that change availability

ConfigurationEffect on this flow
officeHours.timezone, workdays and blocked timesInterpret the requested calendar day in the business's zone; show a closed day as an explicit outcome.
Service duration and breakAfterThe meeting's end and the occupied recovery window differ. The buffer must remain unavailable even though it is not meeting time.
Slot-generation modeDuration-based starts and interval-based starts are different policies. Use server-returned slots; do not reconstruct them by adding a hardcoded30 minutes.
Host selection / bookingTypeAn explicit host must belong to the definition and be available. Leaving the host unspecified lets the configured unassigned/round-robin policy apply. Changing hosts invalidates a previously selected slot.
spots, party limitsAvailable places and allowed party size are distinct constraints. Read the response and preserve server refusal if another customer takes the place.

For a range calendar, POST availability accepts reservationDefinitionId, optional serviceName, from as a business-calendar date and days (default 7, at most 31), plus excludeReservationId when moving an existing booking. The small client uses the one-day slots operation and omits its current occupied start; a range-calendar implementation should use the exclusion-aware operation. Use the existing record's ID to exclude its own occupied place, not another customer's booking. Event, service-point and pipeline definitions have additional setup; this service-client example must not silently relabel them as ordinary timed consultations.

Resolve and render the definition's configured form

The selected definition's data.form points to a collection name, not a submitted form record. The demonstrated reference is cc_intake_1011. Resolve that name through the current repository collection-schema operation and select the matching collection's data.schema. An enriched definition may instead carry a collection object; inspect its data.schema. Do not request broad staff/customer data just to render form fields, and never embed a staff token in the browser to bypass a schema-read refusal.

The collection lookup returns a list: choose the entry whose data.name matches the configured name, then use its data.schema. Do not assume the HTTP response itself is the schema or silently take a different collection’s first result.

The controlled customer session successfully read /repository/collections/cc_intake_1011. Organization enrichment can change the schema, so fetch the current schema when selecting the definition. A failed schema request, a missing named collection, or an enriched form object without a usable schema must stop the booking details step with an understandable retry/error state. The sample rejects these states instead of turning them into an empty form. An absent form means no extra intake; a configured form that failed to load is a different state.

Configured collection properties mapped to the actual native controls

Concept illustration with an actual native capture. Each property keeps its API key while the client renders its human title and required control. The video graphic reveals these relationships one at a time.

The practice form contains Project type, a required choice between Single room and Whole home; What would you like help with?, required text with a 12-character minimum and 300-character maximum; and I agree to be contacted about this booking, a boolean that must be true. Render human titles and descriptions, while retaining property names as the answer keys. The title may change without changing the payload key.

In the reference client, follow this sequence: select Room planning → choose its available time → show the newly resolved intake controls → submit empty → see the relevant field errors without a booking request → choose Single room, enter “Plan a reading corner with accessible shelving.” and accept the contact choice → review the selection and submit. Keep the answers visible after a network or slot conflict so the customer can choose another time without retyping them.

Actual configured intake validation on native iOS

This native test left the required answers empty and verified that no booking POST occurred. The errors come from the fetched schema.

Entered configured answers before resubmission

The captured errors above are rechecked on submission; entering a value does not immediately remove every old error in this recording. The saved-result chapter confirms the completed values.

The downloadable renderer covers text, enum, number/integer and boolean controls and the constraints used here. It is a bounded renderer, not a full JSON Schema engine: unknown schema/field rules (including composition and conditional rules), malformed properties and required names without corresponding controls stop booking. It supports required, enum, const, string length/pattern and numeric minimum/maximum; optional nonnumeric input is still an error in a numeric control. Files, nested schemas, conditional rules and remote data-source controls require their own renderers; the sample identifies unsupported controls and blocks submission instead of silently dropping them. For attachment intake, use File Storage: customer upload, list and delete and stored files versus record references and then pass the resulting field value according to the configured schema. Do not send a device-local file path or invent a second upload protocol.

Validation boundary: client validation makes the form usable; it does not secure a direct API caller. Current reservation creation visibly enforces booking rules and preserves submitted answers, but this audit has not established server enforcement of the referenced form's required/enum/length constraints. If those answers are mandatory for your application, enforce the same approved schema at your trusted application boundary. The successful readback below proves persistence, not an unperformed invalid-payload rejection test.

Construct the booking from the selected state

Payload valueWhere it comes from
reservationDefinitionIdThe selected definition's sk.
serviceThe selected service's name; the slots request calls this value serviceName.
startTime, endTimeExact instants from the selected returned slot, not a newly formatted device-local string.
formThe validated answer map keyed by configured schema property names.
Customer ownershipThe authenticated customer session; do not trust a public owner-ID field.
Optional host/party choicesOnly controls supported by the selected definition and current operation contract.

For the extended example, form is { "projectType": "Single room", "brief": "Plan a reading corner with accessible shelving.", "contactConsent": true }. The form title is not an answer key. Keep these answers nested under form; do not flatten brief into the reservation root or send the schema itself as the answer. Flat booking request fields become savedRecord.data on readback, so the stored answer location is savedRecord.data.form.

Do not copy the whole definition or a previous enriched reservation into a create request. Send the deliberate booking inputs. AppEngine calculates/sets record identity, reservation reference, status, timezone, configured meeting details and other platform fields. Omitting endTime can use the service duration, but retaining the chosen slot's exact endpoints makes the demonstrated selection explicit.

Create and verify the saved booking

Send POST /client-data/reservations with the selected definition, service, slot instants and configured form answers. Notice service, not the slot request's serviceName. The customer's session supplies ownership; do not let a public form choose an arbitrary owner. The response gives sk and the customer-facing reservation reference in data.name.

Read GET /client-data/reservations/:id using the returned sk. Show the saved service, time, timezone, reference, status and data.form answers. Compare each answer to the submitted value; an optimistic local form is not the saved result. Show success after this readback. Hiding a loading indicator or showing an optimistic card alone is not proof of a saved reservation.

Actual Flutter web application after a saved booking

Actual browser result. The saved reference is read from AppEngine.

Continue on web and native

The same customer signing in to the same organization on another device reads GET /client-data/reservations. The server list and exact detail record provide continuity. No record is copied from browser storage to the phone. The native app is an actual Flutter iOS build; a narrow browser window is not evidence of mobile execution. Android uses the shared source but is not yet independently tested in this course.

The native test passed signin, availability, create, reread, another customer's exclusion, reschedule, cancellation, refresh and signout. Sanitized native outcome. The browser-created booking was then opened, rescheduled and cancelled in the actual iOS application; its complete changed and cancelled cards were inspected after recording.

The same booking in the actual native iOS application

Actual native app capture, not a resized browser.

Handle availability conflicts and unknown writes

If another customer takes a displayed time, the create request can return 400. Preserve the response explanation and reload availability. Do not turn a refused request into an empty booking list. For a transport interruption, the server may have saved a record even though the client never received the response. Inspect the customer's saved list before repeating a create. Do not automatically retry uncertain writes. In this client, open My bookings and compare the intended service, start time and submitted answers with the saved records. If the result is ambiguous, retain the entered values while investigating; absence from one stale screen is not proof that the write failed.

The native/browser reference shows a loading state during network work and disables duplicate actions. User choices are separate from waiting for network: the reschedule dialog must not retain an endless progress indicator while waiting for a click.

Reschedule the same record

Use POST /crm/reservations/reschedule/:id with a fresh startTime and reason. The customer token is checked against ownership. The practice request uses notify:false; it does not claim an email was delivered. Read back the same ID, verify the same reference, changed start/end and stored reschedule history. Keep the current booking visible until the change is confirmed.

A reschedule conflict currently returns 409 with reason: slot_unavailable and problems; selecting the unchanged start without a service change returns 400 with reason: same_time. A move keeps the previous duration unless a new service is explicitly selected, in which case AppEngine derives the duration from that service. The reference UI changes time only. Customer clients cannot use a staff override to ignore capacity or host constraints. Host/party controls, paid checkout and switching service while moving are extension paths discussed here; the shipped UI demonstrates unassigned, one-place, free-service time changes, not those unrun variants.

The other customer's attempt to reschedule this ID returned 404. A full or closed destination is a refusal, not a completed move. The application's Change time control fetches fresh availability before offering choices.

Actual native record after rescheduling

The reference YPBLZHAQ remains unchanged while the saved time changes from 09:00 to 09:45. The configured answers remain alongside that same record.

Cancel and retain history

The established cancellation route is DELETE /crm/reservations/cancel/:email/:reservationCode. Encode both values as path components. This is an email-and-code capability route; it does not prove bearer ownership. The client first reads the owned detail and takes email/code from that saved record. A server gateway should enforce that preflight server-side too. The preflight does not change the direct API's access model.

After cancellation, reread by ID and display cancelled. The saved record remains in history. A repeat cancellation returns the existing cancelled result. Encode the saved data.name reference, not the record sk, in the cancellation path. Deleting a card locally is not cancellation. Avoid the legacy client-data delete shortcut whose argument forwarding does not match this contract.

Actual native cancelled booking and retained history

Verify customer separation

Sign in as a second customer in an independent session. Their list must omit the first customer's booking. Use separate customer tokens and the same organization; changing only the UI label is not a second identity. Reading its ID through the customer-owned detail route returned 404; authenticated rescheduling returned 404 as well. Missing and inaccessible IDs intentionally share this result. These tests establish those particular owned routes, not every legacy email/reference route.

Refresh, restore and sign out

The refresh request uses refresh_token at POST /profile/customer/refresh. Update stored credentials and prove the refreshed session by another profile/owned-reservation read. The official authentication callbacks provide renewal; on a refused renewal return to signin. Signout clears this client's session and is not a claim of global revocation across devices.

The official package distinguishes current signout from remembered accounts. signOut() clears the active token and cached person, but leaves remembered accounts in the session store. For a “forget this account on this device” action, call forgetSession(email, orgId) as well; that removes the matching remembered entry and does not revoke tokens on other devices. The sample does not expose a remembered-account picker, so do not imply that button exists.

Native secure storage enables restoration on a later launch. The sample partitions its store by organization, so changing organizations must select the matching session store and credentials, not reuse the previous organization’s token. Browser memory intentionally does not survive reload. Test the next visit separately from first signin when choosing your deployed session arrangement.

Equivalent requests and implementation checklist

The download includes Flutter, TypeScript, JavaScript, Python, curl, JSON and Postman examples. They preserve HTTP statuses and raw JSON. The smaller HTTP clients illustrate the reservation contract; use the full official authentication layer or linked Authentication lesson for verification/challenge completion. The source package excludes private defines and generated builds.

Before extending this client, check each outcome: the selected definition and service produce the expected times; configured questions load and validate; a saved booking contains the exact answers; the same customer reads it on another device; rescheduling retains identity and answers; cancellation remains in history; a second customer cannot read or move the owned record. Check a failed schema read and an unavailable time as well as the successful path. These are platform integration checks, not just whether a screen looks complete.

Revision evidence and remaining capture

Actual saved answers read back on native iOS

The native configured-form journey independently saved THXZN0SP after changing services, verifying required fields blocked the POST, and filling the fetched schema. It reread all answers, then cancelled and confirmed the answers remained. production/qa/native-form-result.json records the calls and assertions. The browser version independently saved YPBLZHAQ with these answers. That exact booking was then read, moved and cancelled on native iOS, with every answer preserved; production/qa/form-cross-device-result.json records the result. The completed raw captures still require the revised narrated edit.

The configured-form API proof saved reservation CU6IWC1R, reread all three answers exactly and retained them after cancellation. Evidence is in videos/appengine-connected-client-usecase/production/lab/form-flow/. The revised client now exposes definition/service selection, schema resolution and intake validation. Native and browser controls and maintained-reference navigation have now been captured. Those raw clips and the revised explanation need to replace the old video middle; they have not yet been assembled into a new narrated export. The previous cut remains available for review, with its missing coverage clearly labelled. No new narration/export is claimed by this written revision.

Demo source code

AppEngine Web and Mobile booking client on GitHub. Runnable Flutter web/iOS booking app with the official client snapshot, configured forms, session handling, rescheduling and cancellation; JavaScript, TypeScript and Python HTTP examples included.

The downloadable ZIP is the recording-time snapshot. Use GitHub for the maintained examples. Each app has its own run instructions; no private credentials are included.