← All products

Attendance built around the place you work.

A mobile attendance system combining location and face verification, with a review path for records that need a closer look.

Product
Tapakin
Platform
Mobile · Web
Built with
Flutter, Dart, Go, Gin, PostgreSQL, SvelteKit, Python, FastAPI, Garage S3
Made at
Sevtee, as an independent product
Tapakin logo

The next chapter of Absenku. The product brings employee check-ins and attendance review together; the case study documents the earlier implementation and its development screens.

What it does

Tapakin lets an employee clock in or out only when they are physically at an authorized location and verified as themselves, and it keeps working without a network. It exists to end titip absen, attendance by proxy.

A Flutter app runs five gates on every check-in: GPS accuracy, mock location, geofence, liveness and face match. A Go API verifies, stores and flags. A SvelteKit console lets HR review flagged records and manage offices, people, policies and the monthly recap. A FastAPI face service runs ArcFace as the authority on identity. An HR edition adds leave, rosters, shift swaps and overtime on the same records.

Tapakin continues the product I started as Absenku. The Work case study documents that earlier implementation and its screens.

What was built

  • Face enrollment on the device with a quality gate, passive liveness and a MobileFaceNet embedding, uploaded only with consent; ArcFace embeddings on the server.
  • An offline queue sealed in an HMAC envelope keyed per install, synced idempotently with client-generated IDs.
  • One pure gate pipeline shared by the app and a multi-tenant verification API, so other HR systems can call the same checks with API keys, webhooks and metering.
  • An impossible-travel gate: speed between this fix and the last accepted fix, minus both accuracy radii.
  • Tenant isolation by typed IDs plus Postgres row-level security, scoped roles that answer 404 outside their scope, and audited overrides with a mandatory reason.
  • Selfies and documents sealed with AES-256-GCM before they reach S3-compatible storage.
  • The HR edition: leave balances computed on read, one sequential approval-chain engine reused for leave, swaps and overtime, overtime verified against presence with the statutory tier split, and an employee document vault.

Engineering decisions

  • Hybrid recognition. The device detects, checks liveness and matches offline; the server’s ArcFace result is authoritative. Server-only would have killed offline use, and device-only is not auditable.
  • The offline threshold is strict, so offline accepts are a subset of server accepts. A server reject of an offline accept goes to review rather than reversing automatically.
  • A Python sidecar for the face engine, because InsightFace owns the detection and alignment that most affect accuracy, and that was worth a second runtime.
  • Root and emulator flags only inform, because they are client claims. Impossible travel, a server-side fact, decides.
  • One verification store for both paths, reversing an earlier two-store design that cost double persistence on every new gate.

What I learned

Running machine learning on phones from Flutter was new ground: ML Kit, TFLite, and an iOS bring-up where the app had never been compiled for iOS and the first run saw zero faces because of EXIF orientation.

The hardest lesson was a silent fail-open. The on-device model failed to load on both platforms, a stub that compared 8-by-8 brightness took over, and two different people scored 0.85, an accept, where the real model scored 0.25. The fix was small; the lasting changes were bigger. Stored references were relabelled by migration, the attempt log now records which embedder produced each score, and offline refuses with “unavailable” rather than guessing.

I also learned to keep decisions in the open: more than 78 ADRs, several of them reversals, and known-gap notes that record disproved hypotheses so the next person does not repeat them. Storage taught a smaller lesson when the MinIO image stopped being served: encrypt at the application layer and treat the object store as replaceable.

Next productLacakku

Have something in mind?

Let’s make it work.