This is the in-app home / how-to-use docs page. It is not the place page.
Fibgle is an internal-only, byte-faithful digital twin of the Google Business Profile API, so rosie's eventual real Google integration can run unmodified against a stable URL. It clones the shape of the Google surface — the same host/path shapes, byte-identical envelopes, and the exact edges rosie branches on — not the product. As the topbar says: internal twin, not Google. This is a testing tool, not a public Maps/Google product.
There are two server-rendered surfaces; do not confuse them:
GET / (this page) — the how-to-use docs / landing page you are reading now.GET /m/locations/:locationId — the public "looks like Google" place page
(a Google-style business card + reviews), plus its /write composer and the
POST that inserts a simulated review. That surface mimics Google; this one explains Fibgle.rosie points its Google client at Fibgle by overriding the Google hosts. Because Fibgle answers on the same host/path shapes with byte-identical envelopes, rosie cannot tell it apart from Google — no client code changes, just a base-URL swap.
Recommended topology: a single path-routed Worker answering every Google surface; fallback: four custom domains (one per Google host).
Standing caveat: the base-URL premise is currently undemonstrated / blocked
pending rosie's real GBP client. rosie has no business.manage GBP client yet — only
better-auth Google login — so the single-Worker topology is recommended-pending-confirmation, not
proven end-to-end. Fibgle itself faithfully answers the Google shapes regardless of which topology rosie
eventually wires up.
Documentation-altitude walkthrough (prose only — there are no live forms on this page).
/o/oauth2/v2/auth, then exchange the code at
/token for an access + refresh pair. Replaying the same authorization code within ~120s is
idempotent and returns the same token pair./v1/accounts, then their locations
at /v1/accounts/:accountId/locations. readMask is REQUIRED on the
Business Information v1 reads (absent/empty → 400 INVALID_ARGUMENT)./v4/accounts/:a/locations/:l/reviews. Walk nextPageToken to the last page
(nextPageToken is absent on the final page) and keep rows where
updateTime > cutoff since the previous poll.PUT .../reviews/:reviewId/reply. The upsert bumps the parent review's
updateTime, so the row resurfaces on the next incremental poll./token
(grant_type=refresh_token) and retry once. A 403 is terminal (permission denied) — do
not retry.Several non-Google paths put data into the twin:
POST /admin/seed/pack
with {pack:"nl-restaurants"} (or us-diners, or a raw pack body) creates
N accounts + locations with varied locales/categories and a starting review distribution, and
returns every minted id. This is the fastest way to exercise accounts.list paging
and a multi-account catalog.POST /admin/accounts creates one account +
a connectable grant; POST /admin/accounts/:accountId/locations adds a location under
it. (The same thing in one JSON file: the create-account / create-location
scenario ops, linked to reviews by named ref.)/m/locations/:locationId/write composer and post a review using the mock identity picker.POST /admin/seed/baseline installs the
baseline world; POST /admin/seed runs JSON scenarios (create/reply/edit/delete/bulk/time-travel);
the single-row /admin/reviews/* endpoints edit one review; and
POST /sim/tokens/expire force-expires access tokens to fire the
401 → refresh → retry path.Invariant: every settable field round-trips through
GET .../reviews, every mutation recomputes the location's
averageRating / totalReviewCount, and a provisioned account/location reads
back byte-identically through the v1/v4 surfaces — indistinguishable from a fixture one (one shared write path).
The operator surfaces — /admin, /admin/seed*,
/admin/reviews/*, and /sim/* — are gated by the
FIBGLE_SEED_TOKEN secret via seedGate.
FIBGLE_SEED_TOKEN is unset, these routes return
404 — there is no open console.x-fibgle-seed-token header or a ?token= query param; a wrong or missing token
also returns an opaque 404 (never 401/403, so the surface's existence is not leaked).This page documents only the env-var name and the mechanism — never a token value.
Every route Fibgle serves, grouped by surface. METHOD · PATH · purpose · auth.
Public probes — no auth, no gating, always reachable.
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET | /healthz | liveness; {status:"ok", service:"fibgle"} | public |
GET | /build-info | deploy-proof provenance {commit, buildTime}; Cache-Control: no-store | public |
GET | / | this in-app home / how-to-use docs page | public |
accounts.google.com / oauth2.googleapis.com shapes. No data-API authenticate(); FLAT {error, error_description} envelope.
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET | /o/oauth2/v2/auth | authorize/consent; validates client_id + exact-registered redirect_uri BEFORE redirecting (bad redirect = error PAGE); auto-approve via FIBGLE_AUTO_APPROVE or ?approve=1; ?fibgle_user= picks the mock user | public (own client/redirect check) |
POST | /token | authorization_code (PKCE S256 + ~120s idempotent replay = same pair) and refresh_token grants; refresh omits refresh_token | public (timing-safe client_id+secret) |
POST | /oauth2/v4/token | alias of /token, same handler | public (timing-safe client_id+secret) |
POST | /revoke | resolves an access OR refresh token to its grant, sets grant.revoked=true; empty 200 | public |
GET | /v1/userinfo | OpenID-style userinfo; FLAT OAuth 401 on missing/unknown/revoked/expired; does NOT require business.manage; quotaGate skips this path | bearer (own check, not authenticate()) |
Account Management v1. All call authenticate() (Bearer + business.manage) + quotaGate on /v1/*; google.rpc.Status envelope.
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET | /v1/accounts | accounts.list; bearer user's accounts via account_grants; role per-(user,account); pageSize default/max 20 | bearer + business.manage |
GET | /v1/accounts/:accountId | accounts.get; unowned → 403 PERMISSION_DENIED (not 404), no existence leak | bearer + business.manage |
Business Information v1. authenticate() + quotaGate on /v1/*; readMask REQUIRED (absent/empty → 400 INVALID_ARGUMENT, field=read_mask).
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET | /v1/accounts/:accountId/locations | locations.list; readMask REQUIRED; accepts accounts/{id} or accounts/-; pageSize default 10 max 100; totalSize only when a filter is present | bearer + business.manage |
GET | /v1/locations/:locationId | locations.get (bare name); readMask REQUIRED; 404 if missing, 403 if caller doesn't own the location's account | bearer + business.manage |
Legacy GMB v4 — the poll surface. authenticate() + resolveLocation ownership + quotaGate on /v4/*; starRating word-enum (FIVE); aggregates on every page; google.rpc.Status.
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET | /v4/accounts/:accountId/locations/:locationId/reviews | reviews.list; keyset paging, orderBy whitelist, aggregates every page, nextPageToken ABSENT on last page; pageSize ≤50 | bearer + business.manage (+ ownership) |
GET | /v4/accounts/:accountId/locations/:locationId/reviews/:reviewId | reviews.get; NOT_FOUND (404) if absent | bearer + business.manage (+ ownership) |
PUT | /v4/accounts/:accountId/locations/:locationId/reviews/:reviewId/reply | reviews.updateReply UPSERT {comment,updateTime}; bumps parent updateTime | bearer + business.manage (+ ownership) |
DELETE | /v4/accounts/:accountId/locations/:locationId/reviews/:reviewId/reply | reviews.deleteReply; {} on success, NOT_FOUND if no reply | bearer + business.manage (+ ownership) |
POST | /v4/accounts/:accountId/locations:batchGetReviews | batchGetReviews (P2 shape-only stub); {locationReviews:[{name,review}]} no aggregates; rosie does not call it | bearer + business.manage (+ per-batch ownership) |
/m SSR simulation surfaces. No authenticate(), no seedGate(); mock identity picker; histogram is UI-only.
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET | /m/locations/:locationId | SSR place page (card + reviews + UI-only histogram); HTML 404 page if missing | public |
GET | /m/locations/:locationId/write | SSR composer with persona picker; optional ?rating=N; HTML 404 page if missing | public |
POST | /m/locations/:locationId/reviews | insert one review (P0 simulation write), 303 back to place page; bare-JSON 404 (not HTML) if missing | public |
Hidden, NON-Google. Gated by the FIBGLE_SEED_TOKEN secret via seedGate. Secure default: 404 when the token is UNSET (no open console). When set, callers must present the matching token (x-fibgle-seed-token header or ?token=) or also get an OPAQUE 404 (never 401/403). Mechanism + env-var name only — never a value.
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET | /admin | SSR operator dashboard (per-location title/count/avg) | seed-token (404 when unset/wrong) |
POST | /admin/seed/baseline | idempotently install baseline world; {ok:true, seeded:"baseline"} | seed-token |
POST | /admin/seed | run a JSON scenario (raw body or dashboard __json): create-account/create-location (named refs) + review create/reply/edit/delete/bulk/time-travel | seed-token |
POST | /admin/seed/pack | stand up a believable multi-business world in one call: {pack:"nl-restaurants"|"us-diners"} or a raw pack body → N accounts + locations + a review distribution | seed-token |
POST | /admin/accounts | create one account + a connectable grant (idempotent on a supplied accountId) | seed-token |
POST | /admin/accounts/:accountId/locations | create one location under an existing account (404 if the account is unknown) | seed-token |
POST | /admin/reviews/:reviewId/reply | operator upsert reply {comment} (404 if no row, 400 if empty) | seed-token |
POST | /admin/reviews/:reviewId/edit | operator edit {starRating?, comment?} + bump updateTime (404 if not found) | seed-token |
POST | /admin/reviews/:reviewId/delete | operator delete by id (404 if not found) | seed-token |
POST | /sim/tokens/expire | force-expire access token(s) {accessToken?} (one or ALL) to trigger 401→refresh→retry | seed-token |
batchGetReviews is a shape-only stub rosie does not call.averageRating +
totalReviewCount, never a histogram.?fibgle_user=.FIBGLE_TOKEN_SIGNING_SECRET (tokens are
opaque, validated by D1 lookup), and no KV (OAuth codes live in D1).docs/proposals/google-business-clone.md.apps/fibgle/docs/ — on any disagreement, the as-built docs win.Liveness / provenance probes: GET /healthz and
GET /build-info. The read-only place pages live at
GET /m/locations/:locationId.