top of page

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:

  1. Child security label

    • first name + last name

    • room / class

    • date/time

    • allergy/alert icons if enabled

    • shared security code

    • optional barcode/QR

  2. Guardian pickup label

    • session/date

    • big security code

    • optional barcode/QR

    • count of linked children

  3. Name tag

    • name only

    • org/event branding

    • no security code

  4. 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

  1. local print agent for reliability

  2. direct network printer for supported models

  3. 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

  1. session overview

  2. attendance by room

  3. first-time guests

  4. family attendance history

  5. 6-week attendance report

  6. birthdays in attendance range

  7. volunteer attendance

  8. check-out compliance / still checked in

  9. 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.

 

Givehub.com

GiveHub.com
Acworth, GA 30101

United States
Email: sales@givehub.com
Phone: 866-933-7048

 

MISSION STATEMENT

 

To offer a robust and a superior product suite to help non-profits and churches increase giving and operate more effectively and efficiently with cutting edge technology. 

 

Yours in Christ, 
GiveHub.com Team

  • Facebook Social Icon
  • LinkedIn Social Icon
  • Instagram Social Icon

© 2026 GiveHub.com - All rights reserved - Support - Book a Demo

bottom of page