---
type: SQLite Table
title: shift_signups
description: An employee's claim on a shift slot — approved or cancelled.
resource: backend/src/db/index.ts
tags: [data-model, sqlite, shift-planning]
timestamp: 2026-08-16T00:00:00Z
---

# shift_signups

# Schema

| Column | Type | Description |
|---|---|---|
| `id` | INTEGER PK | autoincrement |
| `slot_id` | INTEGER | references [shift_slots](./shift-slots.md) |
| `employee_id` | INTEGER | references [employees](./employees.md) |
| `status` | TEXT | `approved` \| `cancelled` |
| `created_at` | TEXT | ISO 8601 UTC |
| `cancelled_at` / `cancelled_by` | TEXT \| NULL | when, and by whom (admin email or the employee's own) |

Cancelling never deletes the row (`status = 'cancelled'`), so who was ever
signed up for a slot stays visible in history.

# Signup rules

Enforced in `signupInternal` (`backend/src/services/shiftPlanning.ts`), in
this order, identically whether the employee signs themselves up or the
admin assigns them:

1. Slot exists and isn't `closed`.
2. Not already approved for this exact slot.
3. **No time overlap** with any of the employee's other approved slots,
   anywhere — not just the same day. Computed from real start/end
   `Date`s (`slotStart`/`slotEnd`), so a slot whose `end_time <= start_time`
   correctly counts as ending the next day.
4. Slot isn't already at capacity.

All of this runs synchronously in a single request — better-sqlite3 is
synchronous, so there's no `LockService`-style mutex needed the way the
source Apps Script version required
(`signupEmployeeToSlot_` in `gscript/ShiftPlanningService.js`).

# Related

- [shift_slots](./shift-slots.md)
- [Shift planning API](/docs/api/shifts-routes.md)
