---
title: "Architecture"
description: "How the Flutter picker is layered: native widgets, the scope and controller, one picker snapshot, and the drawn SeatLayer venue map."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.seatlayer.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture

The Flutter picker is one native widget tree over one drawn map, joined by a
single seam. Understanding that seam is what makes the customisation and
composition guides obvious rather than magical.

## The layers

![The Flutter picker stack: the ready-made SeatLayerPicker and your own composition both sit on SeatLayerPickerScope, which owns the controller and the picker snapshot; those drive the SeatLayer venue map, which reaches the SeatLayer API.](/diagrams/flutter-layers.svg)

| Layer | What lives there | Who owns it |
| --- | --- | --- |
| Your app | Routes, navigation, checkout, analytics | You |
| Picker widgets | Header, legend, dock, confirm card, cart, 3D chrome | Flutter, yours to change |
| Scope and controller | `SeatLayerPickerScope`, `SeatLayerPickerController`, `SeatLayerPickerState` | The SDK |
| Venue map | The drawn venue, pan, pinch, 3D scene | SeatLayer |
| API | Live inventory, holds, best available | SeatLayer |

Two rules bind the whole design:

1. **The venue map owns the venue; native owns the chrome.** The map knows
   Flutter owns the furniture, so it draws no seat tooltip, test-mode badge, or
   attribution of its own. Exactly one of each is drawn in Flutter.
2. **A row name may already contain its section.** The SDK prints row labels
   with the section prefix removed, so a card reads `Stalls D · Row C` rather
   than `Stalls D · Row Stalls D C`.

## The snapshot is the seam

State reaches Flutter as a complete replacement — never a patch. Dart ignores
stale state and serialises inventory-changing actions, so a repeated checkout
tap cannot produce two holds.

| Snapshot field | What it carries |
| --- | --- |
| `event` | `key`, `name`, `venue`, `mode`, `currency`, `startsAt`, `salesClosed` |
| `branding` | `brandName`, `logoUrl`, `attributionRequired`, organizer palette |
| `categories` | `key`, `label`, `color`, `priceMin`, `priceMax`, `available`, `tiers` |
| `sections` | `id`, `label`, `displayLabel`, `zoneId`, `color`, `dominantCategoryKey`, `seatsLeft`, `priceMin`, `priceMax` |
| `zones` | `id`, `label`, `color` — plus `bestAvailableZones` |
| `map` | `rung`, `focusedSectionId`, `focusedSection`, `viewMode`, `buyerView`, `floors`, `activeFloorId`, `floorMode`, `categoryFilter`, `accessibilityFilter`, `colorblindSafe`, `canZoomIn`, `canZoomOut`, `viewportInsets` |
| `selection` | `SelectedSeat` per seat: label, section, row, seat number, price, tiers |
| `cartLines` | `SeatLayerCheckoutLineItem` per line, with `cartTotal` and `ticketCount` |
| `hold` | `active`, `expiresAt`, `owner` — never the hold id |
| `capabilities` | Which optional features are available on this event |
| `accessStatus` | Whether buyer access is configured, and why not |

The map snapshot's `rung` is the vocabulary the whole picker navigates by:
`venue` is the overview, and `seats` means a section is focused and its seats
are revealed.

> **The hold id is not in the snapshot**
>
> `hold` reports only whether a hold is active, when it expires, and whether the
> picker or your app owns it. The `holdId` itself crosses into Dart exactly once,
> in `SeatLayerCheckoutHandoff`, so no widget or log line can leak it by
> accident.

## Commands go the other way

`SeatLayerPickerController` is a `ValueNotifier<SeatLayerPickerState>` and the
only thing that drives the map. Its surface is typed Dart, not raw strings:

| Group | Methods |
| --- | --- |
| Navigation | `focusSection`, `overview`, `setRung`, `setFloor`, `showAllFloors` |
| Selection | `selectObjects`, `deselectObjects`, `selectCategories`, `deselectCategories`, `removeObject`, `clearSelection`, `setSelectableObjects`, `setMaxSelection`, `setSeatTier` |
| Filters | `setCategoryFilter`, `setAccessibilityFilter`, `setLimitedViewHidden`, `setColorblindSafe` |
| View | `setViewMode`, `setBuyerView`, `showSeatIn3D`, `openSeatView`, `set3DNavigationMode`, `zoomIn`, `zoomOut`, `zoomToFit` |
| Inventory | `bestAvailable`, `setGeneralAdmissionQuantity`, `setTableQuantity`, `resumeHold`, `extendHold` |
| Handoff | `checkout`, `rejectCheckoutHandoff`, `releasePickerOwnedHold` |
| Surface | `setViewportInsets`, `setMapInteractionEnabled`, `setCartSheetExpanded`, `setThemeMode` |
| Lifecycle | `synchronize`, `retry`, `setLifecycle`, `close`, `destroy`, `dispose` |

Read state without owning a controller through the scope's static lookups —
`SeatLayerPickerScope.stateOf(context)`,
`SeatLayerPickerScope.controllerOf(context)`,
`SeatLayerPickerScope.brightnessOf(context)`, and
`SeatLayerPickerScope.stringsOf(context)` among them. See
[building your own layout](/buyer-sdk/flutter/custom-layout) for how a custom
widget uses them.

## From a tap to a handoff

![Sequence across four lanes — buyer, native chrome, drawn map, your server: the buyer taps a seat, the map raises a selection event, a snapshot arrives and the native confirm card appears while the map is made inert, the buyer confirms, select and hold commands go out, the hold comes back inside the next snapshot, and checkout returns a typed handoff to your callback.](/diagrams/flutter-sequence.svg)

Two details in that flow are load-bearing.

**The map is made inert, not just ignored.** While native decision chrome is
up, the picker makes the map itself inert, and a Flutter `IgnorePointer` sits
over it as a second layer. Both are required: the map surface can be hit-tested
beneath composited Flutter chrome even when the Flutter child ignores pointers,
so the originating tap would otherwise select a second seat underneath. As soon
as the prompt closes, the map owns pan and pinch again.

**Native chrome tells the map where it can draw.** `setViewportInsets` reports
how much of the map surface your chrome covers, so the map frames a focused
section, the venue overview, and a best-available result into the part the
buyer can actually see. It is a framing inset, not a clip — the venue still draws and
pans underneath. The drop-in reports its own rail and dock automatically; a
custom layout reports its own.

## The back-navigation ladder

![The back ladder: an expanded cart sheet collapses to its peek, an open confirm card dismisses, a focused section returns to the overview, and only at the overview is your route allowed to pop.](/diagrams/flutter-back-ladder.svg)

One `PopScope` owns all four rungs, so Android predictive back and the iOS edge
swipe take the same path a buyer expects — and a hold already handed to your app
is never released by a close, because ownership moved with the handoff.

## The picker fails closed

Each SDK release is pinned to a matching SeatLayer renderer version, so an app
on a given `seatlayer` version always gets the same renderer. Where a feature
is unavailable — an optional one you turned off, or one an older renderer
predates — the control that drives it is absent rather than decorative. The
picker never shows a button that silently does nothing.

That gating is what makes new features additive. `SeatLayerFloorStrip` draws
nothing on a venue with fewer than two floors; `SeatLayerSeatViewChrome` prints
the seat-view caption natively only where the map has handed those words over,
and otherwise leaves the map to draw them. An app pinned to an older release
behaves exactly as it did before.

## Where credentials live

Buyer access is negotiated at the SeatLayer renderer's own origin, and any
token stays in memory. It never appears in a URL, in a snapshot, or in an
application event, and the picker's own state model has no field that could
carry one. Your app never holds a SeatLayer secret; for gated inventory it
supplies a callback that asks *your* backend to mint a short-lived,
origin-bound buyer session.

## Next steps

- [Customise the picker](/buyer-sdk/flutter/customise)
- [Build your own layout](/buyer-sdk/flutter/custom-layout)
- [Back to the Flutter quick start](/buyer-sdk/flutter)

Source: https://docs.seatlayer.io/buyer-sdk/flutter/architecture/index.mdx
