GIVEHUB CHECK-INS — FULL ARCHITECTURE + CODE BLUEPRINT FOR LEAP
1) Product goal
Build a Planning Center–style secure check-in system inside GiveHub that supports:
-
self check-in kiosks
-
staffed check-in
-
household lookup by phone / name
-
child + volunteer + guest check-in
-
pre-check-in from mobile app
-
QR code scan at station
-
instant label printing
-
parent/guardian matching tags
-
room / class assignment
-
multiple stations per event/session
-
reporting / attendance history
-
label themes / label-only vs security label modes
-
org-specific branding and flows
This should be a native GiveHub product, not a thin integration.
2) Core design decision
Source of truth
Use this model:
-
People.GiveHub stores:
-
person identity
-
family / household relationships
-
guardians
-
contact info
-
permissions
-
optional child notes / allergy / medical flags
-
authorized pickup people
-
-
Check-Ins module stores:
-
events
-
sessions
-
station configs
-
room assignments
-
check-in instances
-
security codes
-
labels printed
-
check-out / pickup logs
-
reporting snapshots
-
-
Webhooks are downstream only:
-
person.checked_in
-
person.checked_out
-
household.prechecked_in
-
label.printed
-
first_time_guest.checked_in
-
room.capacity_reached
-
Do not make kiosk check-in depend on webhook fetches.
3) Main user flows
A. Walk-up self check-in by phone number
Parent walks up to kiosk:
-
enters phone number
-
sees household members
-
selects who is attending
-
system shows eligible rooms / times if needed
-
taps Print
-
system creates check-in records
-
generates one shared household security code
-
prints child labels + one guardian pickup label
B. Mobile pre-check-in
Parent opens GiveHub mobile app:
-
sees upcoming church service / event
-
taps Pre-Check-In
-
selects children attending
-
optionally selects room/time if required
-
app generates short-lived QR code tied to precheck token
-
at kiosk, parent taps Pre-Check-In
-
station opens camera
-
parent shows QR code
-
station validates token
-
station prints immediately
-
token becomes consumed or partially consumed depending on design
C. Staffed check-in
Volunteer or staff:
-
searches family or person
-
can add guest
-
can override room assignment
-
can print labels
-
can mark notes / exceptions
D. Check-out / pickup
Staff checks parent tag:
-
enter security code or scan guardian tag barcode/QR
-
system shows linked checked-in children
-
user confirms pickup
-
check-out records saved with verified_by user/station
E. Label-only mode
For some events:
-
print name tags only
-
no security matching
-
no pickup tracking
-
optional custom tag content
4) Product modules
Module 1: Check-In Setup
Per org, support:
-
station templates
-
label templates
-
themes
-
printer assignments
-
room/location setup
-
check-in policy rules
Module 2: Event / Session Management
Event can have:
-
one or many sessions/times
-
one or many rooms
-
child / volunteer / guest flows
-
capacity limits
-
eligibility rules by age / grade / tags
Module 3: Kiosk App
Optimized for:
-
iPad
-
Surface / touchscreen
-
browser kiosk mode
-
fast print workflow
-
camera QR scan
Module 4: Parent Mobile App
Supports:
-
family members
-
pre-check-in
-
QR code
-
view prior attendance
-
emergency contact / pickup permissions eventually
Module 5: Admin + Reporting
Supports:
-
dashboard
-
attendance by event/session/room
-
first-time guest reporting
-
late pickup tracking
-
6-week / year-to-date attendance
-
export CSV/PDF
5) Database architecture
Below is a recommended schema. Naming can be adjusted to match GiveHub conventions.
5.1 People / household layer
households
create table households (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
name text not null,
primary_phone text,
primary_email text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
household_members
create table household_members (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
household_id uuid not null references households(id) on delete cascade,
person_id uuid not null,
role text not null check (role in ('guardian','child','adult','student','guest')),
is_primary_guardian boolean not null default false,
created_at timestamptz not null default now(),
unique (household_id, person_id)
);
people_checkin_profiles
Check-in-specific profile data, separate from general person record.
create table people_checkin_profiles (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
person_id uuid not null unique,
household_id uuid references households(id),
date_of_birth date,
grade text,
gender text,
allergy_notes text,
medical_notes text,
checkin_notes text,
photo_url text,
security_hold boolean not null default false,
can_self_checkin boolean not null default false,
default_checkin_type text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
authorized_pickups
create table authorized_pickups (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
child_person_id uuid not null,
pickup_person_id uuid not null,
relationship text,
is_active boolean not null default true,
notes text,
created_at timestamptz not null default now(),
unique (child_person_id, pickup_person_id)
);
5.2 Check-in configuration layer
checkin_event_configs
Extends a GiveHub event or can stand alone if needed.
create table checkin_event_configs (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
event_id bigint not null,
enabled boolean not null default true,
mode text not null default 'security'
check (mode in ('security','nametag_only','attendance_only')),
allow_self_checkin boolean not null default true,
allow_staffed_checkin boolean not null default true,
allow_precheckin boolean not null default true,
require_household_match boolean not null default true,
allow_guest_add boolean not null default true,
auto_print_on_precheckin_scan boolean not null default true,
family_code_strategy text not null default 'shared_per_household_per_session'
check (family_code_strategy in ('shared_per_household_per_session','per_child','per_checkin_batch')),
label_template_id uuid,
guardian_label_template_id uuid,
theme_id uuid,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (organization_id, event_id)
);
checkin_sessions
A concrete session/time for check-in.
create table checkin_sessions (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
event_id bigint not null,
name text not null,
starts_at timestamptz not null,
ends_at timestamptz not null,
precheckin_opens_at timestamptz,
precheckin_closes_at timestamptz,
checkin_opens_at timestamptz,
checkin_closes_at timestamptz,
is_active boolean not null default true,
created_at timestamptz not null default now()
);
checkin_rooms
create table checkin_rooms (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
event_id bigint not null,
session_id uuid references checkin_sessions(id) on delete cascade,
name text not null,
code text,
min_age_months int,
max_age_months int,
min_grade text,
max_grade text,
capacity int,
allow_overflow boolean not null default false,
label_color text,
sort_order int not null default 0,
created_at timestamptz not null default now()
);
checkin_station_templates
create table checkin_station_templates (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
name text not null,
mode text not null check (mode in ('self','staffed','pickup','admin')),
theme_id uuid,
label_template_id uuid,
guardian_label_template_id uuid,
allow_phone_lookup boolean not null default true,
allow_name_lookup boolean not null default false,
allow_qr_scan boolean not null default true,
allow_guest_add boolean not null default false,
created_at timestamptz not null default now()
);
checkin_stations
create table checkin_stations (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
name text not null,
station_template_id uuid references checkin_station_templates(id),
event_id bigint,
session_id uuid references checkin_sessions(id),
station_key text not null unique,
assigned_printer_id uuid,
theme_id uuid,
is_active boolean not null default true,
last_seen_at timestamptz,
created_at timestamptz not null default now()
);
checkin_printers
create table checkin_printers (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
name text not null,
type text not null check (type in ('dymo','brother','zebra','star','epson','system')),
connection_method text not null check (connection_method in ('local_agent','network','browser')),
device_identifier text,
paper_size text,
dpi int,
is_active boolean not null default true,
created_at timestamptz not null default now()
);
checkin_themes
create table checkin_themes (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
name text not null,
background_type text not null default 'solid' check (background_type in ('solid','image')),
background_value text,
primary_color text,
secondary_color text,
accent_color text,
text_color text,
button_style jsonb,
logo_url text,
created_at timestamptz not null default now()
);
checkin_label_templates
create table checkin_label_templates (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
name text not null,
type text not null check (type in ('child','guardian','nametag','volunteer')),
width_inches numeric(4,2),
height_inches numeric(4,2),
printer_type text,
template_json jsonb not null,
is_default boolean not null default false,
created_at timestamptz not null default now()
);
5.3 Transaction layer
checkin_precheckins
create table checkin_precheckins (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
household_id uuid not null references households(id),
session_id uuid not null references checkin_sessions(id),
created_by_person_id uuid not null,
status text not null default 'pending'
check (status in ('pending','consumed','expired','cancelled','partial')),
qr_token_hash text not null unique,
expires_at timestamptz not null,
created_at timestamptz not null default now(),
consumed_at timestamptz
);
checkin_precheckin_people
create table checkin_precheckin_people (
id uuid primary key default gen_random_uuid(),
precheckin_id uuid not null references checkin_precheckins(id) on delete cascade,
person_id uuid not null,
room_id uuid references checkin_rooms(id),
attendance_type text not null default 'regular'
check (attendance_type in ('regular','guest','volunteer')),
unique (precheckin_id, person_id)
);
checkin_batches
One print action / family batch.
create table checkin_batches (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
event_id bigint not null,
session_id uuid not null references checkin_sessions(id),
household_id uuid references households(id),
security_code text,
station_id uuid references checkin_stations(id),
source text not null check (source in ('phone_lookup','name_lookup','precheckin_qr','staffed','api')),
precheckin_id uuid references checkin_precheckins(id),
created_by_person_id uuid,
created_by_user_id text,
created_at timestamptz not null default now()
);
checkin_records
create table checkin_records (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
batch_id uuid not null references checkin_batches(id) on delete cascade,
event_id bigint not null,
session_id uuid not null references checkin_sessions(id),
room_id uuid references checkin_rooms(id),
station_id uuid references checkin_stations(id),
person_id uuid not null,
household_id uuid references households(id),
attendance_type text not null check (attendance_type in ('regular','guest','volunteer')),
status text not null default 'checked_in'
check (status in ('checked_in','checked_out','cancelled')),
security_code text,
checked_in_at timestamptz not null default now(),
checked_out_at timestamptz,
checked_out_by_user_id text,
checked_out_station_id uuid references checkin_stations(id),
pickup_verified_by text,
pickup_method text check (pickup_method in ('guardian_tag','manual_code','staff_override')),
notes text
);
printed_labels
create table printed_labels (
id uuid primary key default gen_random_uuid(),
organization_id bigint not null,
batch_id uuid references checkin_batches(id) on delete cascade,
checkin_record_id uuid references checkin_records(id) on delete cascade,
station_id uuid references checkin_stations(id),
printer_id uuid references checkin_printers(id),
label_type text not null check (label_type in ('child','guardian','nametag','volunteer')),
payload jsonb not null,
print_status text not null default 'queued'
check (print_status in ('queued','sent','printed','failed')),
printed_at timestamptz,
error_message text,
created_at timestamptz not null default now()
);
6) Security code strategy
Best approach:
For one household print batch:
-
generate a short, easy, human-readable code
-
same code appears on all child labels in that batch
-
one guardian tag prints with same code
-
optionally include barcode or QR too
Recommended format:
-
4 or 5 chars
-
exclude confusing chars like O/0, I/1
Example generator:
N3E5, T7K2, R8M4
This should be unique at least within:
-
org + session + active checked-in children
Suggested unique index:
create unique index uniq_active_security_code_per_session
on checkin_batches (organization_id, session_id, security_code);
When all linked children are checked out, code can later be reused in future sessions.
7) QR pre-check-in design
Important rule
The QR should not directly expose raw IDs.
Use:
-
signed token
-
or random token with hashed lookup in DB
Recommended:
-
create random token
-
store sha256(token) in checkin_precheckins.qr_token_hash
-
mobile app shows raw token as QR
-
station hashes scanned token and looks up pending precheckin
QR payload options
Simple option:
{
"t": "raw_precheck_token"
}
Or even just the raw opaque string.
Token lifecycle
-
created when parent completes pre-check-in
-
expires at session close or after a configurable window
-
one-time use by default
-
mark consumed after print
-
optional partial state if only some labels printed
8) Station / kiosk architecture
Station modes
Each station can run in one of these modes:
-
self
-
staffed
-
pickup
-
admin
Kiosk routes
/checkin/kiosk/:stationKey
/checkin/kiosk/:stationKey/lookup
/checkin/kiosk/:stationKey/precheckin
/checkin/kiosk/:stationKey/household/:householdId
/checkin/kiosk/:stationKey/print
/checkin/pickup/:stationKey
/checkin/admin/:eventId
Kiosk capabilities
Self station
-
phone keypad
-
optional last-name search
-
pre-check-in button
-
camera scan QR
-
print
Staffed station
-
staff login
-
advanced search
-
add guest
-
override room
-
override capacity with permission
-
reprint label
Pickup station
-
scan guardian tag or enter security code
-
display linked children
-
check out
-
override with staff role if needed
9) Mobile app requirements
Add a Check-Ins tab to GiveHub mobile app for org members.
Parent app screens
-
upcoming sessions
-
household members
-
select children
-
select room/time if needed
-
confirm pre-check-in
-
show QR code
-
view prior pre-check-in
-
optional cancel before arrival
Mobile pre-check-in rules
-
only guardians tied to household can pre-check-in children
-
session must be within allowed window
-
child must meet room eligibility
-
token must expire automatically
-
app should cache QR locally in case signal is bad
10) Label printing design
Label types
Support these out of the gate:
-
Child security label
-
first name + last name
-
room / class
-
date/time
-
allergy/alert icons if enabled
-
shared security code
-
optional barcode/QR
-
-
Guardian pickup label
-
session/date
-
big security code
-
optional barcode/QR
-
count of linked children
-
-
Name tag
-
name only
-
org/event branding
-
no security code
-
-
Volunteer tag
-
name
-
role
-
room assignment
-
Template engine
Store label layouts as JSON:
{
"elements": [
{ "type": "text", "field": "person.full_name", "x": 10, "y": 12, "fontSize": 18, "fontWeight": "bold" },
{ "type": "text", "field": "room.name", "x": 10, "y": 38, "fontSize": 12 },
{ "type": "text", "field": "batch.security_code", "x": 210, "y": 10, "fontSize": 24, "rotate": 90 },
{ "type": "barcode", "field": "batch.security_code", "x": 180, "y": 60, "format": "CODE128" }
]
}
Print architecture
Best design:
-
kiosk sends print job to backend
-
backend builds label payload
-
local print agent or browser printer bridge prints to assigned printer
Preferred methods
-
local print agent for reliability
-
direct network printer for supported models
-
browser print fallback for basic mode
11) Printer agent architecture
Since you already work with kiosk hardware, I would recommend a GiveHub Print Agent.
Print Agent responsibilities
-
installed on kiosk device or front desk machine
-
registers with station/printer
-
polls or listens for jobs
-
converts label JSON to printer-specific output
-
prints
-
reports success/failure
Suggested job flow
-
kiosk submits /api/checkin/print-batch
-
backend creates printed_labels rows with queued
-
agent polls /api/checkin/print-jobs?stationKey=...
-
agent prints
-
agent POSTs success/failure back
Supported printer phases
Phase 1:
-
DYMO
-
Brother QL
-
Zebra basic
-
browser print fallback
12) Room assignment logic
When a child is selected for check-in:
-
find eligible rooms for session
-
match age/grade
-
check capacity
-
apply priority rules
-
auto-assign if only one valid room
-
ask user if multiple valid rooms
Pseudo logic:
function getEligibleRooms(childProfile, sessionRooms) {
return sessionRooms.filter(room => {
const ageOk = matchesAge(childProfile.dateOfBirth, room.min_age_months, room.max_age_months);
const gradeOk = matchesGrade(childProfile.grade, room.min_grade, room.max_grade);
const capacityOk = room.allow_overflow || room.currentCount < room.capacity;
return ageOk && gradeOk && capacityOk;
});
}
13) Reporting architecture
Your screenshots show this clearly: reporting is a major piece.
Admin dashboard
Per event/session show:
-
total checked in
-
regular / guest / volunteer
-
checked out count
-
first-timers
-
by room
-
by time/session
-
pre-check-in vs walk-up
-
label print failures
-
no-show pre-check-ins
Reports to build
-
session overview
-
attendance by room
-
first-time guests
-
family attendance history
-
6-week attendance report
-
birthdays in attendance range
-
volunteer attendance
-
check-out compliance / still checked in
-
custom exports CSV/PDF
Suggested reporting tables/materialized summaries
If needed for scale:
-
checkin_daily_summaries
-
checkin_session_summaries
But you can start with indexed live queries first.
14) Permissions
Roles
-
superadmin
-
org admin
-
check-in manager
-
check-in volunteer
-
room leader
-
pickup staff
-
parent/mobile user
Examples
Parent/mobile user
-
pre-check-in own household only
-
cannot view other families
Check-in volunteer
-
search households
-
check in
-
print
-
no full reporting exports unless allowed
Pickup staff
-
view checked-in children
-
check out
-
no config access
Org admin
-
all config
-
stations
-
rooms
-
themes
-
label templates
-
reports
15) API design
Here is a clean API shape Leap can build.
Session/config
GET /api/checkin/events/:eventId/config
POST /api/checkin/events/:eventId/config
GET /api/checkin/events/:eventId/sessions
POST /api/checkin/events/:eventId/sessions
GET /api/checkin/sessions/:sessionId/rooms
POST /api/checkin/sessions/:sessionId/rooms
Stations
GET /api/checkin/stations/:stationKey/bootstrap
POST /api/checkin/stations
PATCH /api/checkin/stations/:stationId
POST /api/checkin/stations/:stationId/heartbeat
Household lookup
POST /api/checkin/lookup/phone
POST /api/checkin/lookup/name
GET /api/checkin/households/:householdId
POST /api/checkin/households/:householdId/add-guest
Pre-check-in
POST /api/checkin/precheckin/create
GET /api/checkin/precheckin/:precheckinId
POST /api/checkin/precheckin/validate-qr
POST /api/checkin/precheckin/:precheckinId/cancel
Check-in + print
POST /api/checkin/batches/create
POST /api/checkin/batches/:batchId/print
POST /api/checkin/checkouts/by-code
POST /api/checkin/checkouts/by-scan
POST /api/checkin/checkouts/:recordId
POST /api/checkin/reprint/:batchId
Reporting
GET /api/checkin/reports/session-overview?sessionId=...
GET /api/checkin/reports/attendance?eventId=...
GET /api/checkin/reports/first-timers?eventId=...
GET /api/checkin/reports/six-week?eventId=...
GET /api/checkin/reports/export.csv?report=...
Printer jobs
GET /api/checkin/print-jobs?stationKey=...
POST /api/checkin/print-jobs/:jobId/ack
POST /api/checkin/print-jobs/:jobId/complete
POST /api/checkin/print-jobs/:jobId/fail
16) Example backend TypeScript code
Below is starter-style code Leap can adapt.
Security code generator
const CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
export function generateSecurityCode(length = 4): string {
let code = "";
for (let i = 0; i < length; i++) {
code += CODE_CHARS[Math.floor(Math.random() * CODE_CHARS.length)];
}
return code;
}
Pre-check token generator
import crypto from "crypto";
export function generateOpaqueToken(): string {
return crypto.randomBytes(24).toString("base64url");
}
export function hashToken(token: string): string {
return crypto.createHash("sha256").update(token).digest("hex");
}
Household lookup by phone
export async function lookupHouseholdByPhone(db: any, organizationId: number, phone: string) {
const normalized = phone.replace(/\D/g, "").slice(-10);
const result = await db.query(`
select h.id as household_id,
h.name as household_name,
p.id as person_id,
p.first_name,
p.last_name,
cp.date_of_birth,
cp.grade,
hm.role
from households h
join household_members hm on hm.household_id = h.id
join people p on p.id = hm.person_id
left join people_checkin_profiles cp on cp.person_id = p.id
where h.organization_id = $1
and regexp_replace(coalesce(h.primary_phone, ''), '\\D', '', 'g') like '%' || $2
order by hm.role, p.first_name, p.last_name
`, [organizationId, normalized]);
return result.rows;
}
Create pre-check-in
export async function createPrecheckin(db: any, input: {
organizationId: number;
householdId: string;
sessionId: string;
createdByPersonId: string;
selectedPeople: Array<{ personId: string; roomId?: string; attendanceType?: string }>;
expiresAt: string;
}) {
const token = generateOpaqueToken();
const tokenHash = hashToken(token);
await db.query("begin");
try {
const pre = await db.query(`
insert into checkin_precheckins (
organization_id, household_id, session_id, created_by_person_id,
qr_token_hash, expires_at, status
)
values ($1,$2,$3,$4,$5,$6,'pending')
returning *
`, [
input.organizationId,
input.householdId,
input.sessionId,
input.createdByPersonId,
tokenHash,
input.expiresAt
]);
for (const person of input.selectedPeople) {
await db.query(`
insert into checkin_precheckin_people (
precheckin_id, person_id, room_id, attendance_type
)
values ($1,$2,$3,$4)
`, [
pre.rows[0].id,
person.personId,
person.roomId ?? null,
person.attendanceType ?? "regular"
]);
}
await db.query("commit");
return {
precheckin: pre.rows[0],
qrToken: token
};
} catch (err) {
await db.query("rollback");
throw err;
}
}
Validate QR and create check-in batch
export async function consumePrecheckinAndCreateBatch(db: any, input: {
organizationId: number;
stationId: string;
scannedToken: string;
createdByUserId?: string;
}) {
const tokenHash = hashToken(input.scannedToken);
await db.query("begin");
try {
const pre = await db.query(`
select *
from checkin_precheckins
where organization_id = $1
and qr_token_hash = $2
and status in ('pending','partial')
and expires_at > now()
for update
`, [input.organizationId, tokenHash]);
if (!pre.rows.length) {
throw new Error("Invalid or expired pre-check-in QR code");
}
const precheckin = pre.rows[0];
const people = await db.query(`
select *
from checkin_precheckin_people
where precheckin_id = $1
order by id
`, [precheckin.id]);
const securityCode = generateSecurityCode(4);
const batch = await db.query(`
insert into checkin_batches (
organization_id, event_id, session_id, household_id, security_code,
station_id, source, precheckin_id, created_by_person_id, created_by_user_id
)
select $1, s.event_id, $2, $3, $4, $5, 'precheckin_qr', $6, $7, $8
from checkin_sessions s
where s.id = $2
returning *
`, [
input.organizationId,
precheckin.session_id,
precheckin.household_id,
securityCode,
input.stationId,
precheckin.id,
precheckin.created_by_person_id,
input.createdByUserId ?? null
]);
for (const person of people.rows) {
await db.query(`
insert into checkin_records (
organization_id, batch_id, event_id, session_id, room_id, station_id,
person_id, household_id, attendance_type, security_code, checked_in_at
)
select $1, $2, s.event_id, $3, $4, $5, $6, $7, $8, $9, now()
from checkin_sessions s
where s.id = $3
`, [
input.organizationId,
batch.rows[0].id,
precheckin.session_id,
person.room_id,
input.stationId,
person.person_id,
precheckin.household_id,
person.attendance_type,
securityCode
]);
}
await db.query(`
update checkin_precheckins
set status = 'consumed', consumed_at = now()
where id = $1
`, [precheckin.id]);
await db.query("commit");
return batch.rows[0];
} catch (err) {
await db.query("rollback");
throw err;
}
}
Walk-up print batch creation
export async function createWalkupBatch(db: any, input: {
organizationId: number;
eventId: number;
sessionId: string;
householdId: string;
stationId: string;
personIds: string[];
createdByUserId?: string;
}) {
await db.query("begin");
try {
const securityCode = generateSecurityCode(4);
const batch = await db.query(`
insert into checkin_batches (
organization_id, event_id, session_id, household_id, security_code,
station_id, source, created_by_user_id
)
values ($1,$2,$3,$4,$5,$6,'phone_lookup',$7)
returning *
`, [
input.organizationId,
input.eventId,
input.sessionId,
input.householdId,
securityCode,
input.stationId,
input.createdByUserId ?? null
]);
for (const personId of input.personIds) {
const roomId = await autoAssignRoom(db, input.organizationId, input.sessionId, personId);
await db.query(`
insert into checkin_records (
organization_id, batch_id, event_id, session_id, room_id, station_id,
person_id, household_id, attendance_type, security_code
)
values ($1,$2,$3,$4,$5,$6,$7,$8,'regular',$9)
`, [
input.organizationId,
batch.rows[0].id,
input.eventId,
input.sessionId,
roomId,
input.stationId,
personId,
input.householdId,
securityCode
]);
}
await db.query("commit");
return batch.rows[0];
} catch (err) {
await db.query("rollback");
throw err;
}
}
17) Example frontend kiosk logic
Self check-in screen flow
type KioskStep =
| "welcome"
| "phoneLookup"
| "householdSelect"
| "precheckinScan"
| "printing"
| "success"
| "error";
Example kiosk page behavior
async function handlePhoneLookup(phone: string) {
const res = await api.post("/api/checkin/lookup/phone", {
stationKey,
phone
});
if (!res.data.household) {
setError("No household found");
return;
}
setHousehold(res.data.household);
setStep("householdSelect");
}
async function handlePrintSelected(selectedPersonIds: string[]) {
setStep("printing");
const batch = await api.post("/api/checkin/batches/create", {
stationKey,
householdId: household.id,
sessionId: activeSession.id,
personIds: selectedPersonIds
});
await api.post(`/api/checkin/batches/${batch.data.id}/print`, {
stationKey
});
setStep("success");
}
Pre-check-in scan flow
async function handleQrScan(rawToken: string) {
setStep("printing");
const batch = await api.post("/api/checkin/precheckin/validate-qr", {
stationKey,
scannedToken: rawToken
});
await api.post(`/api/checkin/batches/${batch.data.id}/print`, {
stationKey
});
setStep("success");
}
18) Example mobile app flow
Parent mobile check-in CTA
async function createPrecheckin(selectedChildren: string[]) {
const res = await api.post("/api/checkin/precheckin/create", {
sessionId: activeSession.id,
selectedPeople: selectedChildren.map(personId => ({ personId }))
});
setQrToken(res.data.qrToken);
setPrecheckin(res.data.precheckin);
}
QR rendered with token:
<QRCode value={qrToken} size={240} />
19) Suggested UI based on your screenshots
Kiosk screen 1
Large buttons:
-
Enter Phone Number
-
Pre-Check-In
-
Staff Login
Kiosk screen 2
Phone keypad:
-
0-9 keypad
-
clear / backspace
-
big submit button
Kiosk screen 3
Household list:
-
parent name at top
-
family members with photo if available
-
checkbox next to each
-
room/class shown under each child
-
“Add family member / guest” only if enabled
-
print button at bottom right
Kiosk screen 4
Pre-check-in scan screen:
-
camera view
-
“Show your QR code”
-
fallback button “Type code”
Print success screen
-
“Labels printing...”
-
then success confirmation
-
auto reset to welcome screen in 5–8 seconds
Admin screens
-
stations
-
labels & themes
-
rooms & capacities
-
reporting dashboard
-
session live view
20) Important product behaviors
Duplicate prevention
Prevent duplicate active check-in for same person/session unless:
-
admin override
-
prior check-in cancelled
-
person checked out and re-check-in allowed
Reprint behavior
Allow reprint only for:
-
staff role
-
or within short grace window
-
track reprints in printed_labels
Capacity behavior
If room at capacity:
-
show alternate eligible room
-
or require staff override
Guest flow
Allow add guest at station if enabled:
-
first name
-
last name optional
-
guardian phone
-
allergy note optional
-
create lightweight person + profile
Offline / poor signal
At minimum:
-
mobile app caches QR token
-
kiosk should degrade gracefully
-
full kiosk offline mode can be later phase
21) Webhooks
Emit after commit:
checkin.precheckin_created
checkin.precheckin_consumed
checkin.batch_created
checkin.person_checked_in
checkin.person_checked_out
checkin.label_printed
checkin.label_failed
checkin.capacity_alert
Payload example:
{
"event": "checkin.person_checked_in",
"organizationId": 1,
"eventId": 35,
"sessionId": "uuid",
"roomId": "uuid",
"personId": "uuid",
"householdId": "uuid",
"batchId": "uuid",
"securityCode": "N3E5",
"checkedInAt": "2026-03-22T15:25:00Z"
}
22) Rollout phases for Leap
Phase 1 — core MVP
-
event/session setup
-
rooms
-
phone lookup self kiosk
-
household selection
-
shared security code
-
child + guardian labels
-
local printer integration
-
basic reporting
-
staffed check-in
-
check-out by security code
Phase 2 — pre-check-in mobile
-
parent mobile flow
-
QR token generation
-
kiosk scan + instant print
-
pre-check-in reporting
-
reprint + token lifecycle
Phase 3 — advanced administration
-
themes
-
label designer
-
multiple station templates
-
guest flow improvements
-
room capacity rules
-
live dashboards
Phase 4 — premium extras
-
volunteer scheduling tie-in
-
emergency texting
-
classroom rosters
-
live child count boards
-
pickup audit trails
-
offline support
23) Exact recommendation to Leap
Here is a clean copy/paste version you can send:
We want GiveHub Check-Ins built as a native product, similar to Planning Center Check-Ins, but fully integrated with People.GiveHub, Events, CRM, messaging, and reporting.
CORE PRODUCT REQUIREMENTS
1. People.GiveHub remains the source of truth for:
- person identity
- households/families
- guardians
- child profiles
- authorized pickups
- contact info
- optional allergy/medical/check-in notes
2. Check-Ins module owns:
- event check-in configuration
- sessions/times
- rooms/locations
- station templates
- stations
- label templates
- themes
- pre-check-ins
- check-in batches
- check-in records
- printed labels
- check-outs
- reporting
3. Support these check-in flows:
- walk-up self check-in by phone number
- staffed check-in
- mobile pre-check-in
- QR scan at kiosk
- instant label printing
- child pickup / check-out by shared security code
- name-tag-only mode for non-child events
MOBILE APP / PRE-CHECK-IN
Parents must be able to use the GiveHub mobile app to:
- view eligible upcoming sessions/events
- select their children
- pre-check them in
- receive a QR code
- present that QR at the kiosk
- kiosk scans QR and prints immediately
The QR token should be opaque and short-lived, not raw IDs.
Store a token hash in DB and validate at scan time.
KIOSK REQUIREMENTS
Support station modes:
- self
- staffed
- pickup
- admin
Self kiosk flow:
- enter phone number OR choose pre-check-in
- if phone lookup: show household members and allow selection
- if pre-check-in: open camera, scan QR, print immediately
- after print: auto reset to welcome screen
Staffed mode:
- advanced family/person search
- guest add
- room override
- reprint
- capacity override with permission
Pickup mode:
- enter security code or scan guardian tag
- show linked checked-in children
- mark checked out
- track pickup method and verified_by
SECURITY CODE MODEL
For one household print action, generate one shared easy-to-read security code such as N3E5.
Print that same code on all child labels and on one guardian tag.
Use uniqueness scoped to org + session + active checked-in records.
LABELS
Need support for:
- child security label
- guardian pickup label
- name tag only
- volunteer tag
Store label templates as JSON-driven layouts so we can support different printer types and custom layouts later.
PRINTER ARCHITECTURE
We want a GiveHub print architecture that supports:
- local print agent preferred
- browser print fallback
- per-station printer assignment
- print job status tracking
- reprint support
Recommended flow:
- backend creates queued print jobs
- local print agent polls or subscribes for jobs
- agent prints and POSTs success/failure
- printed_labels table tracks status
REPORTING
Need reporting similar to Planning Center examples:
- total checked in
- regular / guest / volunteer
- checked out count
- by room
- by session time
- first time guests
- six-week attendance
- yearly attendance
- export CSV/PDF
- pre-check-in vs walk-up usage
- live admin dashboard
DATABASE TABLES NEEDED
households
household_members
people_checkin_profiles
authorized_pickups
checkin_event_configs
checkin_sessions
checkin_rooms
checkin_station_templates
checkin_stations
checkin_printers
checkin_themes
checkin_label_templates
checkin_precheckins
checkin_precheckin_people
checkin_batches
checkin_records
printed_labels
API SURFACE NEEDED
GET/POST /api/checkin/events/:eventId/config
GET/POST /api/checkin/events/:eventId/sessions
GET/POST /api/checkin/sessions/:sessionId/rooms
GET /api/checkin/stations/:stationKey/bootstrap
POST /api/checkin/lookup/phone
POST /api/checkin/lookup/name
POST /api/checkin/precheckin/create
POST /api/checkin/precheckin/validate-qr
POST /api/checkin/batches/create
POST /api/checkin/batches/:batchId/print
POST /api/checkin/checkouts/by-code
POST /api/checkin/checkouts/by-scan
POST /api/checkin/reprint/:batchId
GET /api/checkin/reports/...
KEY IMPLEMENTATION NOTES
- Do not make kiosks depend on webhook fetches or external lookup for core operation
- Use People.GiveHub as identity master
- Use dedicated Check-In tables for operational data
- Webhooks are downstream notifications only
- Prevent duplicate active check-in for same person/session unless override
- Room assignment should support age/grade/capacity logic
- Reprints should be tracked
- Guest add flow should create lightweight records tied to household/guardian
- Mobile pre-check-in tokens should expire automatically
PHASED DELIVERY
Phase 1:
- session setup
- rooms
- phone lookup kiosk
- household selection
- shared security code
- child + guardian labels
- basic reporting
- check-out
Phase 2:
- mobile pre-check-in
- QR scan
- instant print from QR
- pre-check-in reporting
Phase 3:
- themes
- multiple station templates
- label designer
- advanced reports
- classroom dashboards
Please implement this as a full native GiveHub module, not a lightweight integration.
ALSO IMPORTANT:
For GiveHub Check-Ins, we do not need a fully native parent app first.
Recommended approach:
1. Build a mobile-optimized web pre-check-in experience
2. Let the GiveHub mobile app simply open/mirror that secure URL
3. Use SMS for immediate parent alerts in Phase 1
4. Add push notifications later
PARENT PRE-CHECK-IN FLOW
- Parent opens secure mobile web URL
- Auth via existing login or SMS one-time code
- Parent sees eligible household members
- Selects children attending
- Confirms pre-check-in
- Backend creates precheckin record and QR token
- Mobile web page displays QR code
- At kiosk, parent taps Pre-Check-In and scans QR
- Station validates token and prints immediately
STAFF-TO-PARENT ALERT FLOW
We also need staff members to be able to message parents immediately while children are checked in.
Add a Message Parent action from:
- room roster
- live check-in dashboard
- active household/check-in record
Phase 1 alert delivery should be SMS, not app-only push, because SMS is more reliable for immediate parent contact.
Examples:
- Please come to Nursery
- Please come to Room 102
- Your child needs assistance
- Please pick up at check-in desk
DATA MODEL ADDITION
For each active check-in batch, store the active guardian alert contact used that day:
- alert_contact_person_id
- alert_contact_name
- alert_contact_phone
- alert_contact_method
Do not rely only on generic household contact info. We need the actual alert target for the current check-in batch.
RECOMMENDED DELIVERY PHASES
Phase 1:
- mobile web pre-check-in
- QR token generation
- kiosk QR scanning
- SMS parent alerts from staff dashboard
Phase 2:
- mobile app wrapper
- persistent login
- in-app alert center
- push notifications
Phase 3:
- expanded native app functionality if needed
This gives us the fastest path to launch while still supporting a future mobile app strategy.
