{{ vpLabel }}
WEB APPLIVE · ACTIVELY MAINTAINED10 MIN READ

Tasmi’ - Every recitation, on record

A mobile-first web app for a Qur'an recitation program, with two sides. On the participant side, people register for two weekly sessions and log each recitation. On the supervisor side, supervisors verify, correct and report on them.A mobile-first web app for a Qur’an recitation program, with two sides. On the participant side, people register and log each recitation. On the supervisor side, supervisors verify and correct them.

It started in WhatsApp and Google Sheets. The quick first app that replaced them held 54 participants’ records by Batch 5, and lost people’s history three times. More patches would not fix that, so I first defined what every report may and may not do, then redesigned each screen around those rules.

ROLEProduct designer and builder, end to end
PARTNERProgram lead, who set scope and signed off each feature
TIMELINEJul 2026 to presentONGOING
PLATFORMMobile web app, phone-first, two sides in one app
TOOLSSolide (my own design tool), Claude Code (build), SvelteKit + Supabase
SKILLSProduct design, prototyping, front-end
PROBLEMv0 lost participants’ recitation history three times. Supervisors rebuilt reports by hand from WhatsApp and spreadsheets.What went wrong ↓
WHAT I DIDRedesigned the three most-used jobs: registering, reporting, correcting. One question per screen, only what applies, built on rules set first.See the key decisions ↓
RESULTBatch 6 runs entirely on v1. 0 records lost, logging 34% faster, 9 of 11 testers prefer it.See the final design ↓
IMPACT

From v0 to v1: nothing lost, nothing guessed, every fix undoable.Nothing lost, nothing guessed, every fix undoable.

34%Faster to logNo more retyping name and level. Make-up reports dropped from 54 to 24 seconds.
0Records lostv0 wiped history three times. Every v1 fix keeps the old value and can be undone.
9 of 11Prefer v1The other two rated both the same. Nobody picked the old form.
Timed input test, 11 participants, same three tasks on both forms. Medians, first attempt only.
“No need to type my name or level again. Just progress and sick leave. Much easier.”
ANDRI · PARTICIPANT, CONSULTANT · TIMED INPUT TEST
AT A GLANCE
THE APP TODAY · TWO SIDESOne record, two sides: participants report, supervisors review.Two phones for participants, one for supervisors. Each animated mockup plays one flow on its own.
01 · USERS & CONSTRAINTS

Two roles, one phone each, a few minutes around class.

Classes run weekly, in separate male and female groups.
Participants are working adults who report from a phone in the minutes around class.
Supervisors are paid teachers who also hold other jobs.
They teach the classes and correct each participant’s recitation.
Classes run weekly in separate male and female groups. Participants are working adults who report from a phone around class. Supervisors are paid teachers with other jobs.

RESEARCH · THREE SOURCESWhat I listened to, measured and traced before designing.
01LISTEN
Online interviewsShort calls with participants about when, where and how they report.PARTICIPANTS
02MEASURE
Timed input test11 participants ran the same three tasks on v0 and v1. The 34% comes from here.11 TESTERS
03TRACE
Bug logsLogs from v0 and the live app showed where records went missing, and why.V0 + LIVE APP
WHAT IT REVEALED · TWO PERSONASThe interviews kept pointing to two people: one who reports in a hurry, and one who checks everyone’s reports.
USER PERSONA · 1 OF 2
R
RendraThe in-a-hurry reciterParticipant · 35 · Male · Phone only
ROLEParticipant
AGE35
OCCUPATIONField sales, on the road most days
GROUPMale
DEVICEPhone only
TECHLow comfort
SESSIONS2 per week
REPRESENTS 47 PARTICIPANTS
“I just need today to count.” Logs from his phone just before or after class. Not technical, often in a hurry.
GOALSLog today in a couple of tapsSee what is done and which sessions he owesKnow when the next session is
FRUSTRATIONSCopied yesterday’s message, edited it, pasted it backLost track of his own record across a long chat
BEHAVIOURSLogs right after recitingPosts a recap screenshot to the group as proof
USER PERSONA · 2 OF 2
B
BayuThe stretched-thin teacherSupervisor · 40 · Male · Phone, laptop at times
ROLESupervisor
AGE40
OCCUPATIONFull-time engineer, teaches in the evenings
GROUPMale
DEVICEPhone, laptop at times
SESSIONSSeveral per week
TIMELimited, other work outside the program
REPRESENTS 5 SUPERVISORS
“I want to teach, not keep records.” Teaches several sessions a week beside a full-time job, then checks what participants logged.
GOALSEnter how long he taught, fastFix a wrong entry without fearSee one person’s attendance and progress
FRUSTRATIONSJotted reports on paper for daysTyped them into Google Sheets whenever a free weekend came
BEHAVIOURSPosts the daily schedule reminder to the groupShares progress and make-up reminders, weekly or monthlyReviews the batch at the start and end-of-batch meeting
02 · PROBLEM

History went missing three times, and the code had already written down why.History went missing three times, and the code said why.

With WhatsApp and Google Sheets, participants lost track of their own records, and supervisors struggled to log them while teaching.The app was meant to fix that, but then history went missing.

I expected a mystery. Instead, each incident was described in a comment beside the line that caused it.Read together, they all came from one root cause.

ROOT CAUSEZero foreign keys.A participant existed only as a name, typed again on every row.
THE THREE INCIDENTSEach one lost real history, and each traces back to the same root cause.
{{ i.n }}{{ i.tag }}
{{ i.t }}{{ i.d }}{{ i.m }}
TRY IT · FIX ONE WRONG ENTRY{{ owCap }}
COMPARE · SWITCH VERSION
TABLE · RECITATIONSRENDRA'S HISTORY · {{ owCount }} ENTRIES
IDDATETIMENAMESTATUS
{{ r.k }}{{ r.d }}{{ r.tm }}{{ r.n }} Absent→Recitation · p. 12▾ {{ r.s }} {{ r.tag }}
{{ owStepL }}
{{ owQuery }}
{{ owResult }}
PROBLEM STATEMENT

How might participants and supervisors log and fix progress quickly on a phone, in a way they can trust, when the first version let them pick wrong and then lost what they picked?How might participants and supervisors log and fix progress fast on a phone, and trust it, when v0 let them pick wrong and then lost it?

03 · APPROACH

Before writing v1, I audited the plan so it could not repeat v0.I audited the plan so v1 could not repeat v0.

Zero foreign keys was never one bad line of code.
It was a decision nobody wrote down, so nobody checked it
against the others.
Zero foreign keys was never one bad line of code. It was a decision nobody wrote down, so nobody checked it.

v1 was planned as four documents: framing, flows, schema, migration.
Each passed review alone. Read together, they had gaps:
two correct decisions that break the moment they meet.
The first audit found 11. Drawing the decisions as screens found 19 more, and all 30 were closed before the build started.
v1 was planned as four documents. An audit found 11 gaps, drawing the screens found 19 more, and all 30 were closed before the build.

30GAPS FOUND, ALL CLOSED BEFORE BUILD11 in the first audit, 19 more once the decisions were drawn as screens.
Login & audit trail2
Schedules & slots8
Targets & make-up debt6
Batches & moving up9
Leaving the program5
ONE OF THE GAP · S.1 OF 30Three login rules, each correct alone, that made login impossible together.
THE PLAN · THREE RULES THAT EACH PASSED REVIEW
Log in with just a PINSo the list of participant names stays hidden.Hides the name list.
Store the PIN hashedSo a leaked database reveals no PINs.No PIN leaks.
Lock after wrong attemptsCounted on each participant’s own row.Counted per row.
THE GAP · WHY THEY BREAK TOGETHER
With only a PIN, the app must check every participant’s hash one by one: about 20 seconds per login. And since a guesser never names anyone, no row’s counter rises, so the lock never fires.
THE FIX · CLOSED BEFORE BUILD
A keyed lookup column finds the owner in one search. A unique index blocks shared PINs. Wrong attempts count per device.
HOW THE 30 STAY CLOSED

Four rules, read at the start of every session.

I built v1 with an AI coding assistant that forgets everything between sessions. The rules are what carries over.

{{ r.i }}{{ r.t }}{{ r.d }}
{{ r.i }}{{ r.t }}{{ r.d }}
AUDIT · 30 GAPSEvery gap, and how it was closed
IDGAPCLOSED WITH
{{ g.t }}· {{ g.n }}
{{ r.id }}{{ r.gap }}{{ r.fix }}
04 · KEY DECISIONS
DECISION A · REGISTRATION · PARTICIPANT SIDE

Time of day first, exactly two sessions, and an alias instead of a name.Time first, exactly two sessions, an alias instead of a name.

Most people choose by when they are free, so sessions are grouped by time of day. Registration needs exactly two, and the button says how many are still missing.People choose by when they are free, so sessions are grouped by time of day. The button says how many of the two are still missing.

V0Slot grid
Choose schedule slots
Hello Rendra,
please pick 2 time slots you can commit to for recitation.
No slot chosen — pick 2 slots
* All times in WIB
TimeSunMonTueWedThuFri
05.00
3/4
not
open
1/2
not
open
Full (2)
0/2
13.00
not
open
Full (2)
0/2
1/2
0/2
not
open
19.30
1/2
1/3
Full (3)
2/3
1/3
1/2
21.30
2/4
0/4
2/4
3/4
1/4
3/4
BackConfirm
Register
Progress
Admin
V1By time of day
9:41
Tasmi’{{ rgBar }}Change
STEP 3 OF 3

{{ rgTitle }}

{{ rgNote }}
{{ p.l }}{{ p.sub }}
{{ p.l }}{{ p.sub }}
SHOW BY
{{ rgBandTitle }}{{ rgBandMeta }}
{{ rgFullDiv }}
{{ rgMoreL }}
{{ rgBtn }}
V0Hard to scan on a phone. Days × times in one grid of tiny cells. To find a free evening, people had to read every column.
V1Start from when you are free. Sessions are grouped by time of day, or by supervisor. Exactly two are required, and the button counts what is missing. No full name is asked for; an alias clash is fixed by the person.
REJECTED · DAY STRIP
Day stripRejected
REJECTED · ONE LIST
One listRejected
REJECTED · DAY STEPPER
Day stepperRejected
REJECTEDDay strip: seven days in one narrow rowPeople think in morning, noon or evening, not weekdays. Seven days in one narrow row was also hard to tap.
REJECTEDOne list: every option on a single pageThe list grew so long nobody could see its end. People scrolled once to see every option, then again to choose.
REJECTEDDay stepper: one day at a timeIt groups by day, like the strip. With one day on screen, nobody knows which days exist until they keep pressing next.
CHOSENTime of day first, supervisor as a second viewPeople pick by when they are free. Female participants can switch the same tabs to supervisors.
ONE RULE FOR GOING BACK

Looking does not delete. Changing does.

Going back to check your group keeps your sessions. They clear only when the group or class actually changes, and the warning sits inline on that step, instead of a dialog that warned about a loss that had not happened yet.

DECISION B · INPUT BY INTENT · PARTICIPANT SIDE

Ask what happened, then offer only what fits.

The old form asked for a time, then a status, then a mode, and allowed every combination. The Hub now asks one question: what happened today?The old form allowed every combination of time, status and mode. The Hub asks one question: what happened today?

V0Progress input
V1The hub
V0Exhausting, and still open to mistakes. Every report started by picking your own name again, then the Input tab, session time, attendance, level and two page fields: 12 taps plus typing, every session. And a make-up could be logged with no session owed.
V1
Two taps, only valid choices. You are already signed in and today’s session is filled in, so a report is Report now, then Send. Each button appears only when it applies.{{ n.b }} · {{ n.t }}{{ n.d }}
REJECTED · BASE HUB
Base hubRejected
REJECTED · TODAY FIRST
Today firstRejected
REJECTEDThree equal buttons: Report, Leave / sick, Make upMake up carries a debt, so it is a task, not a peer of Leave. Equal weight hid the one thing owed. Today’s session sat in small grey text under a two-line greeting, and the progress card did not look tappable.
REJECTEDToday first, then a separate card for each factIt answered the right question first, but debt, progress, note and schedule became four cards to scan. Participants could not tell which one to act on.
CHOSENOne hub: today’s card, then one card for where you standOne primary action, leave as a link under it, and the debt as a row with its count instead of a button.
Greyed means “not now”, absent means “not yours”. A full session stays greyed, because it is still a real choice for someone. A Report button on a finished term is removed, because greying it would suggest the old class is still yours to attend.
V0Edit record table
DashboardScheduleEdit RecordExamDurationReport
Edit / Delete Progress Records
Choose a participant to view and delete records.
Participant name
Abdurrahman
#DateAttendanceModeLevelPagesExamScoreNotes
1Mon, 3 Aug 2026presentrecitationLevel 31–3———
2Wed, 5 Aug 2026excusedrecitation—————
3Mon, 10 Aug 2026presentrecitationLevel 34–7———
4Wed, 12 Aug 2026absentrecitation—————
5Mon, 17 Aug 2026presentrecitationLevel 38–11———
6Wed, 19 Aug 2026presentrecitationLevel 312–15———
7Mon, 24 Aug 2026presentrecitationLevel 316–19———
8Wed, 26 Aug 2026presentrecitationLevel 320–23———
9Mon, 31 Aug 2026presentrecitationLevel 324–27———
10Wed, 2 Sep 2026presentrecitationLevel 328–31———
11Mon, 7 Sep 2026presentrecitationLevel 332–35———
12Wed, 9 Sep 2026presentrecitationLevel 336–39———
13Mon, 14 Sep 2026presentrecitationLevel 340–43———
14Wed, 16 Sep 2026presentrecitationLevel 344–47———
15Mon, 21 Sep 2026presentrecitationLevel 348–51———
16Wed, 23 Sep 2026presentrecitationLevel 352–55———
Scroll sideways and down to see more
Save
Register
Progress
Admin
V1History + edit sheet
AbdurrahmanMenu
HistoryBatch 6 · 6 Jul – 9 Sep 2026
{{ c.l }} {{ c.n }}
Wed 12 Aug 2026{{ hs.ubSub }}
17–23 AUG 20262 records
Wed19
RECITATION19.30
Level 3 pages 12–15Supervisor Deku
Mon17
RECITATION13.00
Level 3 pages 8–11Supervisor Deku
10–16 AUG 20262 records
Wed12
Mon10
RECITATION13.00
Level 3 pages 4–7Supervisor Deku
Edit recordWed 12 Aug · 19.30 · Abdurrahman
TYPE
PAGES · TILAWATI 3
to
NOTES (OPTIONAL)
DECISION C · SUPERVISOR TOOLS · SUPERVISOR SIDE

A mistake costs one tap to fix, and one tap to undo.

Supervisors used to fix entries from a laptop, in Google Sheets, after a chat message. Now a correction happens on a phone, on the participant’s own history.Supervisors used to fix entries in Google Sheets, on a laptop. Now they fix them on a phone, in the participant’s own history.

ALSO ON THE SUPERVISOR SIDE

Three tools that run on the same rule as the edit above. Supervisors fix records, report the day and promote participants without losing data or doing the work twice.

{{ f.k }}{{ f.t }}{{ f.d }}{{ f.s }}
V0A one-row fix meant fighting a table. Nine columns, one row per session. On a phone only three fit, so the supervisor scrolls sideways for level, pages and notes, and down to find the row. Save commits the whole table at once.
V1Fix one row, safely. A correction touches only that row. The old value stays, with who changed it and when, and the way back sits beside it.
REJECTED · BASE HISTORY
Base historyRejected
REJECTED · WEEKLY TIMELINE
Weekly timelineRejected
REJECTED · MONTH CALENDAR
Month calendarRejected
REJECTED · DENSE LIST WITH COUNTS
Dense listRejected
REJECTEDBase history: every record carries the same weightSessions, absences and waiting periods share one card style, so the record to correct does not stand out.
REJECTEDWeekly timeline: too plain to edit fromRecords read as a log. Nothing shows a row can be tapped to correct it, and low contrast flattens the hierarchy.
REJECTEDMonth calendar: dates are easy to find, records are notDots say something happened, not what. Reaching a record takes an extra tap, and a day with two records hides one.
REJECTEDDense list: density hides the tap targetFitting more rows made each one smaller and lighter. Nothing signals a row opens for editing, and the hierarchy is unclear.
REJECTEDFill the page numbers in automaticallyAn invented number would stay in someone’s history for good, and a partly invented history cannot be trusted.
REJECTEDLeave Present out of the correctionsA wrong absence could only become Sick. The debt disappears, but the session that really happened is never recorded.
CHOSENPresent asks for the pages, like the participant’s own formThe supervisor records what was actually recited. Try it on the 12 Aug row above.
DECISION D · WHOLE-PROGRAM PROGRESS · SUPERVISOR SIDE

One table that is always current.

To see how everyone stood since the program began, the program lead had to total a sheet by hand. Now it is one live table.The program lead used to total a sheet by hand. Now it is one live table.

V0Totals from sheets
Rekap Batch 5 (copy) (2)Last edit was 6 days ago
fx=COUNTIFS('Laporan Tilawati'!A:A;A2;'Laporan Tilawati'!F:F;"hadir";'Laporan Tilawati'!H:H;"<>ujian")&"/20"
A
B
C
D
1
Nama
Level
Sesi
%
2
Abdurrahman
L3
12/20
60%
3
Mahesa
L3
17/20
85%
4
Arka
L3
18/20
90
5
Dimas
L2
#REF!
6
Fauzan
L2
15/20
75%
7
Ilham
L1
9/20
45%
8
Raka
L1
16/20
80%
9
Yusuf
L3
14/20
70%
10
Hafiz
L2
19/20
95%
11
Naufal
L1
11/20
5,5%
12
Zaki
L3
20/20
100%
13
Rendra
L3
18/20
90%
14
Bima
L2
13/20
65%
15
Rafi
L1
IkhwanAkhwatRekap totalRekap (lama)
V1Progress table
Participants & progressMenu
Progress table
Male participants’ progressBatch 6 · up to 19 Sep 2026 · safe target 80%
3safe
1below
1not started
Tilawati (3)
NAMEMATERIALSESSIONS%
AbdurrahmanLevel 312/2060%
MahesaLevel 317/2085%
ArkaLevel 218/2090%
Pra Tilawati (2)
NAMEMATERIALSESSIONS%
DimasTahiyyat15/1883%
FauzanAl-Fatihah0/0—
Copy as image
V0Nobody knew where anyone stood. Totals were rebuilt by hand each batch, in a copy of a copy of a sheet, and were out of date once done.
V1Always current, nothing to rebuild. The table counts live from the records, leads with a summary, and shares as an image.
DEEP DIVE · SYSTEM, SHIPPING, CODE
05 · DESIGN SYSTEM & ACCESSIBILITY

Rust color means "you can still catch up", never "you failed".

Everything this app flags can be fixed: a missed session can be made up,
a wrong PIN can be typed again. So there is no red anywhere.
Warm neutrals carry the screen, green is for action,
and rust only marks what is behind.
Everything this app flags can be fixed, so there is no red anywhere. Warm neutrals carry the screen, green is for action, and rust marks what is behind.

Then a WCAG 2.1 AA audit against all five backgrounds failed five tokens.
Each was deepened in the same hue, not swapped,
and primary was deepened too so the soft green could clear 3:1
against both white and primary.
A WCAG 2.1 AA audit failed five tokens. Each was deepened in its own hue, not swapped, primary included.

{{ g.title }}{{ g.sub }}
{{ w.tk }}{{ w.hex }}
{{ r.k }}{{ r.t }}{{ r.d }}{{ r.s }}
AUDIT · WORST CONTRAST ACROSS FIVE BACKGROUNDS
{{ a.tk }}{{ a.oc }}→{{ a.nc }}
Border itself was not darkened. It stays a decorative hairline, and the new outline token carries every edge a control needs to show. Primary already passed at 8.79 and was deepened only so primary-soft could clear 3:1 against both white and primary.
06 · FINAL DESIGN

Same jobs, fewer steps, nothing to reconcile.

The jobs did not change between versions.
What changed is how many decisions each one asks of the person,
and where the record ends up.
The jobs stayed the same. What changed is how many decisions each one asks, and where the record ends up.

Pick scheduleParticipant side
ExamsSupervisor side
Progress tableSupervisor side
Participants & progressMenu
Progress table
Male participants’ progressBatch 6 · up to 19 Sep 2026 · safe target 80%
3safe
1below
1not started
Tilawati (3)
NAMEMATERIALSESSIONS%
AbdurrahmanLevel 312/2060%
MahesaLevel 317/2085%
ArkaLevel 218/2090%
Pra Tilawati (2)
NAMEMATERIALSESSIONS%
DimasTahiyyat15/1883%
FauzanAl-Fatihah0/0—
Copy as image
WHAT CHANGED, JOB BY JOB
JOBBEFOREV1
{{ j.k }}{{ j.a }}{{ j.b }}
07 · SHIPPING SAFELY

Move every row, then count them back.

Starting empty would have been the easiest build,
and it would have lost history a fourth time,
after the three incidents in section 02
. Instead v1 launched with Batch 6, and Batch 5 came across as read-only history.
That changed the stakes: old rows only had to be right
to read, not right to calculate.
Starting empty would have lost history a fourth time. Instead v1 launched with Batch 6, and Batch 5 came across as read-only history.

The two old databases looked alike
and meant different things, so the script ran two paths.
On 20 August 2026 it finished, and every number
was read back from the new database against its source.
The two old databases looked alike but meant different things, so the script ran two paths. On 20 August 2026 every number was read back against its source.

0ROWS LOST SILENTLY1,026 entries + 36 exams + 25 quarantined = 1,087, the exact count of old rows.
COUNTED BACK · SOURCE → DATABASE
{{ c.k }}{{ c.v }}{{ c.v }} ✓
FOUR RUNS REJECTED · ZERO ROWS DAMAGED
{{ r.c }}{{ r.d }}
Each time the transaction rolled back whole. Three of the four were my mistakes, not the data's.
SHIPPING · DESIGN TO CODE

The canvas is a test the code has to pass.

Every Tasmi' screen was drawn in Solide, the design tool I built,
and moved into code through its MCP, the bridge that lets an AI agent read and write anything on the canvas, without limits. A drift script compares each frame
with the shipped screen, and known differences are held in a baseline
so new ones cannot hide among them.
Every screen was drawn in Solide, the design tool I built, and moved into code through its MCP. A drift script compares each frame with the shipped screen.

290 HI-FI FRAMES · SNAPSHOT 5 SEP 2026node scripts/canvas-drift.mjs
{{ d.n }} {{ d.l }}
Differences are old debt. The gate lets the number only go down.
WHAT TASMI' TAUGHT SOLIDE

A tool that says “done” must prove it. Solide now reports what it wrote.

Five times in one week, a command reported success and did not do what it was asked. A loud failure costs a minute. A silent one costs a day. Building Tasmi' on Solide turned into the bug report that made its MCP say when it had not written anything. I still check the result with my own eyes, and an extra skill tells the AI to do the same before it says done.

Five times in one week, a Solide command said “done” and wrote nothing. Now it reports what it wrote.

08 · OUTCOME & REFLECTION

Batch 6 runs entirely on v1.

All 47 participants re-registered through the new flow,
and all 5 supervisors work from it. The outcomes below come from the database
and from what supervisors and participants
reported after the switch.
All 47 participants and 5 supervisors now work in the new flow. The outcomes below come from the database and from what users reported.

{{ o.v }}{{ o.t }}{{ o.d }}
IN THEIR WORDSWhat supervisors and participants said
{{ p.n }} · {{ p.role }}{{ p.i }}
“
{{ p.q }}
{{ p.i }}{{ p.n }}{{ p.job }}
WHAT REBUILDING TASMI' TAUGHT ME
{{ s.t }}{{ s.d }}{{ s.s }}
WHAT'S NEXT FOR TASMI'One person, more than one role.v1 gives every account exactly one role. Real people don't fit that, so multi-account roles come next.
SUPERVISOR + PARTICIPANTRecite and reviewSome supervisors also recite in the program. They need to log their own recitations without a second login.They log their own recitations without a second login.
PROGRAM LEADObserve both groupsThe program lead wants a read-only view of every participant’s progress across the male and female classes.A read-only view across the male and female classes.
NEXT UP · CASE STUDY 02Solide: one canvas, one AI plan, no second appThe design tool Tasmi' was drawn in, and the bugs Tasmi' found in it.READ NEXT →
Solide: a design canvas and a tokens window on one screen, next to the Solide logo