CONTENTS

    How to Build a Nested List Seat Booking System in Momen

    avatar
    Jamie Chang
    ·June 9, 2026
    ·7 min read

    Building a ticketing application requires an intuitive visual map of seats and a robust backend. Structuring a UI where rows contain multiple interactive seats is visually complex. Furthermore, if your database relies on frontend logic, two users can easily book the exact same seat simultaneously.

    By using Momen's nested List components for the frontend layout and its PostgreSQL-backed database for row-level locking, you can build a scalable, visually accurate, and concurrency-safe seat booking system.

    What Is a Nested List Seat Booking Interface and When to Use It

    A nested list is a UI structure where a primary list (rows) contains an embedded secondary list (individual seats). It translates relational database structures into an interactive visual map.

    Use cases:

    • Cinemas and concert halls

    • Flight seat selection

    This layout requires database-level unique constraints. Many Bubble users report that preventing overlapping bookings becomes fragile using frontend searches. In Momen, if two users click "Buy" simultaneously, the database natively rejects the duplicate request without custom locking scripts.

    How to Build This in Momen

    Project Access Link

    View project

    Introduction

    • Goal: Implement a conflict-resistant seat booking feature using nested lists and unique constraints.

    • Applicable Scenario: Cinema seat selection, stadium ticketing, restaurant reservations, or any grid-based resource scheduling scenario.

    • Core Logic: Render a 2D seat layout using a "List in List" structure (Rows -> Seats). Leverage Database Composite Unique Constraints to guarantee absolute data consistency and prevent duplicate bookings at the database level.

    Steps

    Data Storage

    To build this architecture, we need five distinct tables to manage users, sessions, physical layout, and transaction records.

    Data Model

    1. System-generated table used to track order ownership.

    Field Name

    Type

    Note

    id

    Bigint

    Auto-generated, unique user identifier

    2. Used to enable resource reuse across different times or events.

    Field Name

    Type

    Note

    id

    Bigint

    Auto-generated, distinguishes different events/screenings

    3. The data source for the outer list, defining vertical tiers.

    Field Name

    Type

    Note

    name

    Text

    e.g., "Row B", "Row 1"

    sort_order

    Bigint

    Determines the vertical display order on the screen

    4. The data source for the inner list, defining physical locations.

    Field Name

    Type

    Note

    seat_order

    Bigint

    Determines the horizontal order within a row

    type

    Text

    seat (Valid seat) or none (Aisle/Gap)

    row_id

    Bigint

    Relation: Many-to-One to row

    5. The core transaction table. Uses unique constraints to prevent booking conflicts.

    Field Name

    Type

    Note

    account_id

    Bigint

    Relation: Many-to-One to account. Records the buyer.

    session_id

    Bigint

    Relation: Many-to-One to session.

    seat_id

    Bigint

    Relation: Many-to-One to seat.

    Configuring Unique Constraints

    To prevent data duplication and physical overlap, we configure two constraints:

    1. Seat Physics Constraint: In the seat table, add a Composite Unique Constraint named uk_row_seat using the fields row_id and seat_order. This ensures two seats cannot occupy the same physical coordinates.

    2. Order Logic Constraint: In the order table, add a Composite Unique Constraint named uk_session_seat using session_id and seat_id. This guarantees a specific seat can only be booked once per session.

    Data for rows and seats can be rapidly populated using Momen's "Import" function with an Excel/CSV file.

    UI Construction & Interaction

    Outer List: Rows
    1. Add a List component to the canvas.

    2. In the right Data panel:

      • Data source: Remote

      • Data model: row

      • Sort: Add a sort by sort_order Ascending.

    3. Inside this List, place a Text component to display the row name. Bind its content to {Data source/rowList.../item/name}.

    Inner List: Seats
    1. Add another List component inside the List Row.

    2. Configure the Layout to be horizontal so seats line up side-by-side.

    3. In the right Data panel:

      • Data source: Remote

      • Data model: seat

      • Filter: Add a condition where row_id is Equal to {Data source/rowList.../item/id}. This is the crucial step that nests the seats within their respective rows.

      • Sort: By seat_order Ascending.

    Logic & State Configuration

    Inside the inner List Seat, drop a Conditional view component. We will define multiple states for each seat based on the database context.

    1. Hidden State (Aisles/Gaps)

    Create a case named Hidden.

    • Condition: Check if the seat's type is equal to none.

    • UI: Leave the container empty or invisible to act as a walkway gap in your grid.

    2. Purchased State (Logged-in User's Booking)

    Create a case named Purchased.

    • Condition: Filter the order table where seat_id equals the current seat's ID, session_id equals the current session (e.g., 1), AND account_id equals the {Logged in user/id}. If the Count is Equal to 1, the current user owns this seat.

    • UI: Display an icon or image indicating the user's reserved seat.

    3. Occupied State (Booked by Others)

    Create a case named Occupied.

    • Condition: Filter the order table where seat_id equals the current seat's ID, session_id equals the current session, AND account_id is Not equal to {Logged in user/id}. If the Count is Not equal to 0, someone else owns it.

    • UI: Display a grayed-out or locked icon.

    4. Available State (Default)

    Create a case named Available.

    • Condition: Set this as the default fallback branch.

    • UI: Display an empty checkbox or selectable seat icon.

    Actionflow Construction

    Now we attach logic to the On click events of the corresponding states in the Conditional view.

    Canceling an Order (Purchased State)

    When a user clicks their own purchased seat, allow them to cancel.

    1. Add a Show modal node asking "Confirm Booking Cancelation?".

    2. Add a Delete order node. Filter where account_id equals {Logged in user/id} and seat_id equals the current seat ID.

    3. Add a Switch view case node pointing back to Available.

    Handling Unavailable Seats (Occupied State)

    When a user clicks a seat someone else booked.

    1. Add a simple Show toast node displaying: "Sorry, this seat is already taken".

    Booking a Seat (Available State)

    This is where we handle the high-concurrency conflict checks.

    1. Add a Show modal node: "Confirm Booking?".

    2. Add an Insert order node. Map account_id to {Logged in user/id}, session_id to 1 (or your active session variable), and seat_id to the current seat's ID.

      • Crucial Step: In the On Conflict settings, select the uk_session_seat constraint and set the Action Type to None.

    3. Add a Condition node to evaluate the result of the Insert action.

      • Success Branch: Check if {Action result/Insert order/id} is Not null. If true, show a success Toast and use Switch view case to Purchased.

      • Failed Branch: Check if {Action result/Insert order/id} is Is null (meaning the unique constraint blocked the insertion because someone else just booked it). Show a failure Toast ("Seat already booked") and use Switch view case to Occupied.

    Verification

    To test the conflict-resolution architecture:

    Step 1: Initial Booking (User 1)
    1. Open the preview and log in as User 1.

    2. Select Row B, Seat 2. A confirmation modal appears: "Book Row B, Seat 2?".

    3. Click Yes. The seat icon immediately turns blue (the Purchased state).

    Step 2: Concurrent Session (User 2)
    1. Open a new Incognito window and log in as User 2.

    2. Notice that Row B, Seat 2 is already grayed out (Occupied), proving the conditional container is working.

    3. User 2 then successfully books Row C, Seat 3.

    Step 3: The "Ghost" Selection & Conflict Interception (User 1)
    1. Switch back to User 1's window. Since the page hasn't refreshed, Row C, Seat 3 still appears black (Available).

    2. User 1 attempts to book Row C, Seat 3.

    3. Upon clicking Yes, the database interceptor kicks in:

      • The uk_session_seat constraint blocks the insertion.

      • The Actionflow detects the null ID result and triggers the Failed Branch.

      • A Toast notification appears: "Sorry, this seat is no longer available."

      • State Rollback: The seat icon instantly switches from black to gray (Occupied) without a page reload.

    Step 4: Cancellation & Release
    1. User 1 clicks their blue seat (Row B, Seat 2).

    2. Confirm the cancellation. The Delete action removes the record from the Order table.

    3. The unique constraint is released, and the seat returns to the Available state for all users.

    Check your Database order table to ensure only one record was successfully created for that specific seat and session.

    Try It Yourself And Learn More

    We highly encourage you to clone the provided seat booking project to inspect how the nested lists are bound to the data tables. By opening the project, you can view the exact database structure and test the simultaneous booking logic directly in the visual editor.

    Once you understand the foundation, you can easily scale this logic. Consider integrating Stripe for payment processing or utilizing AI Agents to assist users in finding the best available seats based on their specific preferences.

    Conclusion

    Combining nested List components with a transactional database ensures that your seat booking app is both visually intuitive and structurally resilient. Momen handles the complex UI nesting and the backend concurrency natively, allowing you to focus on the user experience without worrying about data corruption or race conditions.

    Clone project today to see how nested lists and atomic transactions operate together in a real-world scenario.

    Build Your App Today. Start With No Code, Gain Full Control as You Grow.