E-Pondok Developer Reference
The technical side of E-Pondok — concepts, the data model, the kiosk pipeline, security internals, architecture, and the full HTTP API. For step-by-step product setup, see the Setup Guide.
Introduction
E-Pondok is a presence & relations system for the modern pondok. A single face kiosk at the gate records who comes and goes, a digital guest book manages visits, and the dashboard weaves that data into reports and a living map of institutional relationships.
Built for the realities of a pondok: it works in places with unreliable connectivity (offline-first), grows with many pondok (multi-tenancy), and keeps things secure without heavy operational burden.
What you get
- Touchless face attendance with automatic entry/exit direction detection.
- Anti-spoofing — rejects photos, videos, and fake face masks.
- Digital guest book with categories, purpose, and visit contributions.
- Institutional relationship management: contacts, partner orgs, tags, and follow-ups.
- Summary dashboard + attendance analytics.
- Passwordless magic-link login; every kiosk signs its requests.
Core concepts
Pondok = tenant
A pondok is a single isolated tenant. Each pondok gets its own subdomain — for example pondokabc.epondok.id — and its data never mixes with another pondok’s, even though they share one database.
Three surfaces
- Apex — the root domain
epondok.id. Public landing, signup, checkout, and this documentation. - Platform — the subdomain
platform.epondok.id. Super-admin: manage accounts, subscriptions, and provision new pondok. - Tenant — a subdomain
*.epondok.id. The per-pondok admin app (dashboard, attendance, visitors, relations, devices).
Locally, localhost and *.workers.dev are treated as tenant. Append ?surface=platform to the URL to force the platform surface during development.
Roles
A person in a pondok has one of the following roles:
| santri | Student |
| ustadz | Teacher |
| staff | Operational staff |
| pengurus | Pondok committee |
Admin roles in the app: admin (full access including Devices), pengurus, and platform (super-admin at apex).
Kiosk
A kiosk is the gate device — a desktop app that runs the face pipeline and records attendance/visitors. Each kiosk registers its own public key and signs every request.
Quick start
From zero to automatic attendance in five steps.
The five-step product walkthrough (register → subscribe → create pondok → install kiosk → enroll → live), with live previews of every form, now lives in the Setup Guide. This page keeps the technical reference.
Dashboard
The tenant home page. A real-time summary across all modules. Data source: GET /api/dashboard.
Summary cards
- Attendance — present today, absent, late, total population, daily trend.
- Visitors — visits today, this month, lecturer visits over 3 years.
- Relations — new, active partners, potential, strategic, alumni network.
- Follow-ups — number due.
Persons & enrollment
Person = every individual in the pondok (santri, ustadz, staff, pengurus). Full CRUD via /api/persons.
Face enrollment
Enrollment captures a face, converts it to a 512-dimensional embedding, then stores it in Vectorize. On enroll, the checkDuplicates option prevents the same face from being registered twice, and qualityScore records capture quality.
capture frame
→ anti-spoofing (MiniFASNet) // reject photo/video/mask
→ embedding 512-d (InsightFace)
→ POST /api/persons/:id/enrollments
{ embedding, deviceId, qualityScore, checkDuplicates: true } -
POST /api/persons/:id/verify-face— match a new embedding vs the person’s enrollments. -
DELETE /api/persons/:id/enrollments/:eid— deactivate an old enrollment (not a hard delete).
The real face pipeline needs the cargo feature —features real-face. Without it, the kiosk uses mock embeddings in development.
Attendance
The product’s core. The kiosk captures a face at the gate → matches it against Vectorize → entry/exit direction is detected automatically → the event is stored with a confidence score (confidence) and device id (device_id).
Offline-first
The kiosk records attendance even when the network drops, queuing locally, then syncs automatically once back online. No check-in is lost to connectivity issues.
Queries
-
GET /api/attendance— event list (Paged), filterspersonId,role,from,to. -
GET /api/attendance/presence— who is currently inside. -
GET /api/attendance/analytics— daily entries/exits, top late, most punctual, window & role filters.
Visitors
The digital guest book. Each visit has a category, purpose, check-in/out, and a visitor number (visitor_no).
Categories
parent_guardianlecturerteachergovernment_officialcommunity_leaderalumnivendorgeneralother
Visit contributions
Each visit can record contributions for reporting: workshop, guest_lecture, training, mentoring, community_service, research_collaboration, recruitment_opportunity, other — with topic, audience, participants, and outcomes.
Institutional relations
A map of the pondok’s network so it stays alive and traceable.
Contacts
RelationshipContact stores name, institution, position, expertise, status (new / active / potential / alumni / strategic), and notes.
Collaboration tags
CollaborationTag (slug + label) can be freely attached/detached on contacts to group partners — e.g. guest-teacher, scholarship.
Follow-ups
Scheduled follow-ups: contact_id, due_date, assigned_to, status (open / in_progress / completed / cancelled). The overdue filter highlights those past due.
Devices
The Devices page (admin only) manages kiosks. Each kiosk is registered with a globally unique device_uid, an Ed25519 public key, and an optional bound_network.
-
POST /api/devices/register— register a new kiosk. -
POST /api/devices/:id/revoke— revoke access (the kiosk can no longer sync). -
POST /api/devices/:id/activate— reactivate. -
last_seen_attracks the last sync.
device_uid is unique across all pondok (not per-pondok). Never reuse the same UID for two kiosks.
Kiosk desktop
A desktop app (Tauri) installed on the gate device. It runs the entire face pipeline locally: capture frame → anti-spoofing → embedding → match → send the event to the backend.
- Full-screen UI for fast check-in at the gate.
- Persons & devices loaded from the backend using the kiosk token.
- Visual (flash) & audio (TTS) feedback when a face is recognized.
- Builds for
aarch64Android as well (experimental).
Offline-first & sync
Because connectivity at a pondok is often unstable, the kiosk does not depend on the internet to record. The local event queue syncs via device-authed routes (sync) once the connection recovers. The backend accepts idempotently — no duplicates on retry.
1. event recorded locally (kiosk offline)
2. enters the sync queue
3. when online → POST to the sync route (Ed25519-signed)
4. backend verifies signature + dedup
5. dashboard updates immediately Face anti-spoofing
Before an embedding is produced, the frame is checked by MiniFASNet to tell a real face apart from a photo, video, or mask. Only a real face (class index 1) is processed further.
The anti-spoofing model needs raw BGR input in the [0, 255] range — not a normalized tensor. The kiosk pipeline prepares the frame accordingly.
Automatic updates
The kiosk checks /kiosk/update.json for a new version. Releases are stored in the R2 bucket epondok-kiosk-releases and recorded in the kiosk_release table. The built-in Tauri updater downloads & installs the update, signed with the Tauri key. The kiosk account is paired with pin b7e6.
Platform admin
The platform surface (platform.epondok.id) is the super-admin space: provision new pondok, manage subscriptions, and change pondok status (active / suspended). The platform token is separate from the tenant token — the two are different origins.
Multi-pondok
One system, many pondok. Each pondok gets a subdomain & isolated data. Technically: a pondok_id column on every tenant table, a subdomain→pondok resolver, and the JWT carries pondokId so every query is scoped to the pondok that owns the token.
When adding a new tenant table, pondok_id + foreign key must be included from the start. Global constraints & FKs in D1 cannot be arbitrarily rebuilt after the fact.
Pricing & seats
Billing is seat-based (seats) via Polar. Monthly payment, minimum 10 seats.
| Seat range | Per seat / mo |
|---|---|
| 10–29 | $0.90 |
| 30–59 | $0.50 |
| 60–99 | $0.42 |
| 100 | $0.50 |
| 101+ | $0.30 |
The Polar webhook uses the Standard Webhooks (Svix) spec — three headers + base64 signature, not polar-*. cancel = grace period, revoke = immediate. POLAR_WEBHOOK_SECRET is mandatory in production.
Magic-link login
Passwordless login: request a link by email, click to sign in. The link is sent via a Cloudflare Email binding. The password is still stored (PBKDF2) as a fallback and for platform admins.
PBKDF2 is capped at 100,000 iterations on Cloudflare Workers. The PASSWORD_ITERATIONS default already reflects this — do not raise it above the cap.
Device signing
Each kiosk holds an Ed25519 private key and sends its public key on registration. Every sync/attendance request is signed; the deviceAuth middleware verifies the signature before processing. A revoked (revoked) kiosk is rejected outright.
Tenant isolation
Pondok isolation is layered: subdomain → pondok (resolver), pondok_id on every row, and pondokId in the JWT. No tenant endpoint can read another pondok’s data. All traffic is encrypted in transit (TLS).
Architecture
- Cloudflare Workers — edge API, serverless, global.
- D1 — SQLite database, multi-tenant via
pondok_id. - Vectorize — 512-d face embedding index for fast matching.
- Polar — subscriptions & checkout; Standard Webhooks for status.
- R2 — kiosk releases (
epondok-kiosk-releases).
The backend once ran on Bun + Postgres + pgvector, then migrated to Workers + D1 + Vectorize. Old traces may appear in comments or schema — what runs now is the Cloudflare version.
Routing
epondok.id → apex (landing, /docs)
platform.epondok.id → platform (super-admin: provisioning, billing)
*.epondok.id → tenant (per-pondok admin app)
localhost / *.workers.dev → tenant (dev) Data model
Core entities and their key fields:
| Entity | Key fields |
|---|---|
| Person | id, full_name, role, phone, active |
| EnrollmentRow | person_id, device_id, quality_score, active |
| AttendanceEvent | person_id, ts, event_type(entry/exit), confidence, device_id |
| Visitor | visitor_no, name, category, purpose, arrival_ts, departure_ts |
| VisitContribution | type, topic, audience, participants, outcomes |
| RelationshipContact | name, institution, status, tags |
| CollaborationTag | slug, label |
| FollowUp | contact_id, due_date, assigned_to, status |
| Device | device_uid, public_key, status, bound_network, last_seen_at |
| Pondok | id, slug, name, status |
All tenant entities also carry pondok_id for isolation.
API reference
The JSON API behind the app. Every list route returns a paginated envelope:
{
"items": T[],
"total": number,
"limit": number,
"offset": number
} Example authenticated request:
curl -H "Authorization: Bearer [REDACTED:Authorization header] header] \
"https://pondokabc.epondok.id/api/attendance?from=2026-01-01&limit=50" Tenant routes
Authentication
/api/auth/loginPhone + password → { token, user }/api/auth/magic/requestSend a magic-link to email/api/auth/magic/verifyExchange magic-link token → { token, user }/api/auth/meCurrent admin sessionDashboard
/api/dashboardAttendance, visitor, relation, follow-up summaryPondok (public)
/api/pondoksPondok list for the landing directoryPersons
/api/personsList (Paged<Person>) — role, active, q/api/persons/:idPerson detail/api/personsCreate person (full_name, role, phone)/api/persons/:idUpdate/api/persons/:idDelete (soft delete)Face enrollment
/api/persons/:id/enrollmentsPerson's enrollment history/api/persons/:id/enrollmentsEnroll embedding (+ deviceId, qualityScore, checkDuplicates)/api/persons/:id/enrollments/:eidDeactivate enrollment/api/persons/:id/verify-faceMatch embedding vs person's enrollmentsAttendance
/api/attendanceEvent list (Paged) — personId, role, from, to/api/attendance/presenceWho is currently inside/api/attendance/analyticsWindow analytics — daily, top late, most punctualVisitors
/api/visitorsList (Paged) — category, from, to, open, q/api/visitors/:idVisit detail/api/visitorsCheck-in (name, phone, category, purpose, …)/api/visitors/:id/checkoutCheck-out/api/visitors/:id/contributionsVisit contributions/api/visitors/:id/contributionsRecord contribution (type, topic)Relations
/api/contactsContact list (Paged) — q, status, institution, tag/api/contacts/:idContact detail/api/contactsCreate contact (name, phone)/api/contacts/:idUpdate/api/contacts/:id/tagsAttach tag (slug)/api/contacts/:id/tags/:slugDetach tag/api/contacts/:id/follow-upsContact follow-upsFollow-ups
/api/follow-upsList (Paged) — status, overdue/api/follow-upsCreate (contact_id, due_date, assigned_to)/api/follow-ups/:idUpdate status / due dateTags
/api/tagsList collaboration tags/api/tagsCreate tag (slug, label)Devices
/api/devicesList (Paged) — q, status (admin)/api/devices/registerRegister kiosk (device_uid, public_key, bound_network)/api/devices/:id/revokeRevoke access/api/devices/:id/activateReactivatePlatform routes (apex)
Platform (apex)
/api/platform/registerRegister platform account (name, email, password)/api/platform/loginPlatform login/api/platform/magic/requestPlatform magic-link/api/platform/magic/verifyVerify platform magic-link/api/platform/meCurrent platform session/api/platform/slug/checkCheck pondok slug availability/api/platform/checkoutCreate Polar checkout session (seats)/api/platform/pondokList pondok owned by the account/api/platform/pondokCreate a new pondok (slug, name)/api/platform/pondok/:idUpdate pondok name / statusThe kiosk & sync routes are used by the kiosk and protected by an Ed25519 signature, not an admin token. Not documented for direct use.