Building a booking system for my kids' school with Tape

This autumn I used Tape to build a booking system for student - mentor meetings at my kids’ school. On the surface it is “just” a calendar with names and time slots. Under the hood it turned into a fun little Tape architecture exercise with JSON, calculation fields, automations and some unexpected platform constraints.

This article walks through the structure of the system, the legacy Google Sheet we replaced, some technical implementation details, and a couple of dead ends that we hit along the way.


The old system: a shared Google Sheet

Before Tape, everything ran via a single Google spreadsheet that parents and staff all edited by hand:

The sheet had:

  • one big grid for all days and mentors
  • time slots as rows
  • columns per mentor
  • parents typing their learner’s name directly into the cell they wanted

It worked, but it had all the classic shared spreadsheet problems:

  • no real constraints - two parents could type into the same slot at roughly the same time
  • easy to accidentally overwrite or move names
  • no automatic way to see which learners were already booked on another day
  • no easy export or derived views (for example, per mentor summary, per learner overview, room based lists)

The goal of the Tape project was to keep the simplicity of that sheet for parents, while giving staff a proper data model, automations and exports behind the scenes.


The problem we wanted to solve

The school runs 360 style mentor conversations over three days. Constraints:

  • fixed dates - for example 15th, 16th and 17th of December
  • multiple mentors, each in a specific room
  • parents should pick a free slot under the right mentor
  • once a learner is booked with a mentor on one day, they should disappear from the “possible learners” list on other days
  • staff wanted to get away from the fragile shared spreadsheet and have one reliable, queryable source of truth in Tape

So the target architecture was:

  • Tape as backend (objects, records, automations, exports)
  • a simple booking page that feels like picking a cell in a spreadsheet, but actually does proper validation and record creation in the background

Data model in Tape

I ended up with three main record types.

1. Availability

One record per mentor per day. The “canonical” representation of that record is a calculation field that outputs JSON as a string, for example:

{
  "day": "2025-12-15T00:00:00.000Z",
  "mentor": "Suzanne",
  "room": "Barceloneta",
  "possibleLearners": [
    "Lea", "Pau M", "Renee", "Kedem", "Scarlett",
    "Adele L", "Keanu", "Val", "Carmen", "Mia"
  ],
  "availableTimeSlots": [
    "14:00",
    "14:30",
    "15:00"
  ]
}

The JSON is not edited by hand. The calculation field aggregates the raw fields (day, mentor, room, possible learners, time slots) into one JSON structure via JavaScript and returns JSON.stringify(...). Other calculation fields and automations then JSON.parse that string to transform or display it.

2. Bookings

One record per confirmed booking:

  • day
  • mentor
  • learner
  • time
  • backlink to the related availability record

These are created by Tape when a parent clicks a booking link.

3. Potential Bookings

This is the slightly crazy one: for every possible combination of

  • day
  • mentor
  • time slot
  • learner

there is a Potential Booking record.

Each of these records has:

  • the parameters (day, mentor, time, learner, etc) saved in fields
  • a Tape weblink in a JSON object in a text field that runs the booking logic for exactly that combination

In our real setup there are 3 days, 15 mentors, up to 8 slots per mentor per day and 175 learners.

Each learner is preassigned to exactly one mentor. For every combination of

  • day
  • time slot with that mentor
  • learner for that mentor

we create a Potential Booking record.

In the worst case (all mentors using all 8 slots) that is up to:

3 days × 8 slots × 175 learners ≈ 4,200 Potential Booking records

The actual number is a bit lower because not every mentor uses all 8 slots, but it is still thousands of records for what is conceptually a single “book this slot for this learner” action.


Core logic - keeping availability and bookings in sync

The core logic is implemented in calculation fields and automations.

Aggregating availability and bookings

An earlier step aggregates the data into a single JSON blob that looks roughly like this:

[
  {
    "availability": {
      "day": "2025-12-15T00:00:00.000Z",
      "mentor": "Suzanne",
      "room": "Barceloneta",
      "possibleLearners": ["Lea", "Pau M", "Renee", "..."],
      "availableTimeSlots": ["10:00", "11:00", "12:00"]
    },
    "bookings": [
      { "learner": "Lisa", "time": "14:00" },
      { "learner": "Mia",  "time": "15:00" }
    ]
  },
  {
    "availability": { "... second day ..." },
    "bookings": [ /* ... */ ]
  }
]

Removing learners who are booked on any day

In one calculation field I run JavaScript that:

  • collects all booked learners across all days
  • filters possibleLearners of each day against that set

Conceptually:

const allBookedLearners = new Set();

data.forEach(entry => {
  entry.bookings.forEach(b => {
    if (Array.isArray(b.learner)) {
      b.learner.forEach(name => allBookedLearners.add(name));
    } else if (b.learner) {
      allBookedLearners.add(b.learner);
    }
  });
});

const normalized = data.map(entry => {
  const avail = entry.availability;
  const cleanedLearners = avail.possibleLearners.filter(
    name => !allBookedLearners.has(name)
  );

  return {
    ...entry,
    availability: {
      ...avail,
      possibleLearners: cleanedLearners
    }
  };
});

JSON.stringify(normalized);

This is something the original spreadsheet could not do reliably: as soon as someone is booked on one day, they simply disappear from the list of clickable learner links on the other days, so parents only ever see valid options.

Human friendly views

From this JSON we generate:

  • HTML tables for the internal overview
  • parent friendly HTML with a logo, a note to parents and color coded availability
  • CSV / XLSX exports for backup and special cases

All of that is done in calculation fields that use JSON.parse and then format strings.


Frontend - a lightweight booking page with links

Instead of building a complex app, the booking frontend is a simple HTML page generated from Tape.

  • Tape is the backend and source of truth
  • a calculation field builds an HTML page that includes
    • a logo and a note to parents
    • a table of mentors (with the room in brackets)
    • for each time slot, a list of remaining available learners, each shown as a clickable link

Each learner name is not a generic “book now” endpoint. It is a direct link to the weblink of the corresponding Potential Booking record, something like:

https://weblink.tapeapp.com/trigger/6d07d7fb-...-45e7-9a93-b03...

When a parent clicks that link:

  1. Tape runs the weblink in the context of that Potential Booking record
  2. The script reads the parameters (day, mentor, time, learner) from the record
  3. It shows the parent a Booking Confirmation with all the details
  4. It creates a Booking record
  5. It triggers the downstream automations to refresh availability and update the HTML

The user experience is still “click a name in a grid”, but behind that click there is a fairly elaborate record and weblink setup.


Dead ends and platform limitations

On paper this sounded simple. In practice we hit a few Tape platform constraints that are worth mentioning.

1. (Abandoned) webhook approach - no GET support

Early in the project I experimented with a different architecture:

  • use a Tape “catch” webhook as a generic API endpoint

  • call it from the frontend like:

    GET https://.../api/catch/<token>?learner=Lisa&slot=169588399-169588460
    

What you actually get back with a GET is a 404:

{
  "status_code": 404,
  "endpoint": "/api/catch/<token>",
  "error_code": "not_found",
  "error_message": "Cannot GET /api/catch/<token>"
}

Webhooks only accept POST. I briefly had a version where the frontend sent POST requests with JSON bodies and the server side script read everything from payload.body. That prototype worked, but in the end I dropped webhooks entirely in favor of only using weblinks plus records, because:

  • the rest of the system was already very record centric
  • access control and context in weblinks fit better with how Tape is structured
  • mixing two different entry points (weblinks and webhooks) felt unnecessary for this use case

So webhooks are a real limitation worth knowing about, but they are not part of the final implementation.

2. Weblinks do not accept parameters

The second roadblock was with Tape weblinks themselves:

  • I wanted to use one weblink as a generic “book this slot” endpoint
  • idea: call it with a query like .../weblink?slot=...&learner=...

That does not work. Weblinks in Tape do not see query parameters in a way that the script can easily consume. They execute in the context of a record and are not designed as parameterized HTTP endpoints.

That limitation is the reason for the next pain point.

3. Record explosion - one Potential Booking record per possible booking

Because weblinks do not accept parameters, the only way to have a “book this specific slot for this specific learner” action is:

  • create a record that stores all those parameters
  • attach a weblink to that record
  • link to that weblink from the parent facing page

This is why the system needs a separate Potential Booking record for every possible combination of:

  • day
  • mentor
  • time slot
  • learner

In a clean backend world you would have a single endpoint like:

POST /book { day, mentor, time, learner }

In Tape, the equivalent is:

  1. precreate N Potential Booking records
  2. generate N unique weblink URLs in the page
  3. let parents click those URLs

For this project I accepted the tradeoff and:

  • created a dedicated app for Potential Bookings
  • hid it from normal users
  • plan to clean it up periodically after the event

It works reliably, but it is a lot of moving parts for what is conceptually a stateless booking call.


What worked well

Despite the constraints, using Tape as the booking engine had a lot of upsides compared to the old Google Sheet.

  • Single source of truth
    Availability, bookings, exports and communication all live in one place and are modeled explicitly as objects and records, not inferred from arbitrary cell contents.

  • JSON + calculation fields
    Storing flexible JSON blobs in calculation fields and using JavaScript to transform them worked really well for

    • removing booked learners from other days
    • generating different views (internal vs parent facing) from the same data
  • Automations as glue
    Automations connected everything:

    • on booking creation they update availability
    • they generate HTML snippets for emails and pages
    • they produce CSV / XLSX exports staff can download if needed
  • Quick iteration
    Most changes were a matter of editing a calculation field or a JS action and reloading the page, without breaking the parent facing interface.


Wish list for Tape after this project

@Leo

If I translate the dead ends into feature ideas, I end up with three things that would make systems like this easier to build:

  1. GET support for catch webhooks
    Even if POST remains the default, having basic GET support would be handy for simple integrations and debugging.

  2. Parameter aware weblinks
    A way to read query parameters inside a weblink context would remove a lot of boilerplate Potential Booking records that only exist to carry parameters.

  3. Headless action endpoints
    Some kind of “action without record” endpoint (that is not ‘receive a webhook’) that can receive a JSON payload and run a script would make Tape an even more powerful backend for lightweight apps, especially when migrating from simple tools like shared Google Sheets.


If you are thinking about using Tape for school events, bookings or any other scheduling problem, this kind of architecture is very doable. Just be prepared to think in terms of records and automations first, and HTTP endpoints second.

3 Likes