Engineering

A stack list is a commodity. The reasoning is not.

Every software company publishes the same page: a row of logos for the languages and databases it uses. It proves nothing, because the list is almost identical everywhere and none of it explains a single decision.

So this page states the reason instead, and names what was rejected to get there. Several of these choices cost us something — a harder operational burden, a slower answer, a queue somebody has to work. Those are the ones worth reading.

Everything below is either a decision already recorded in our technical requirements or a fact we verified independently. Where a number is a budget the build enforces rather than something measured on a running service, it says so. No system described here is serving a customer today.

JavaScript budget
250 KB gzipped
Teacher and guardian paths; over it fails the build
Key screens interactive
3.0 seconds
4 GB Android, 5 Mbps, 200 ms
Capture with no network
72 hours
No loss, no duplicate, no silent overwrite
Primary hosting
In Bangladesh
Colocation, with a standby in a second facility

Budgets and requirements for software still in build. None is a measurement.

The commodity cloud stack, and what is here instead

Most architecture advice assumes a hyperscaler region next door, a fast phone on a fast link, and a payment rail that behaves. Each assumption below was checked against Bangladesh and replaced.

Assumed stack against this stackSpecification
  • Commodity assumption

    A managed cloud region in the country

    What we do here

    There is no AWS, Azure or GCP region, local zone or edge zone in Bangladesh. The primary database is self-managed on colocated hardware, and that operational burden is budgeted rather than hidden.

  • Commodity assumption

    Object storage and error tracking as hosted services abroad

    What we do here

    Self-hosted object storage with an S3-compatible API, and self-hosted error tracking, both in Bangladesh. An exception payload carries local variables and request bodies, so it is confidential-class data.

  • Commodity assumption

    A client-side bundle serves result day

    What we do here

    Guardian pages are server-rendered and cacheable. Result day is a twenty-thousand-view burst against a single endpoint.

  • Commodity assumption

    Sync resolves every conflict by last write wins

    What we do here

    Conflicts resolve by rule, per entity class. Last-writer-wins silently discards a record somebody drove to a farm to collect.

  • Commodity assumption

    Payments matched by best guess

    What we do here

    Matching is exact. Anything that does not match lands in an exceptions queue that a clerk works.

  • Commodity assumption

    The platform collects the money and forwards it

    What we do here

    The institution is merchant of record. Nimikh is never in the flow of funds.

Six assumptions we did not build onSource: technical requirements

A comparison of design stances, drawn from the technical requirements. It describes what the system is specified to do, not a measured result.

A real constraint solver, and an answer when there is no answer

Building a school’s weekly routine is genuinely NP-hard: two thousand lessons each needing a slot, a competent teacher and a suitable room, with nobody in two places at once.

  • The decisionDecision
    Chosen

    OR-Tools CP-SAT, in its own service

    Because

    It is an anytime solver, which is what surfacing a first feasible answer within sixty seconds actually requires. The service never touches the database — it receives a self-contained model and returns a solution — which keeps the isolation boundary out of it entirely.

    Rejected

    Bespoke heuristics, which give neither an optimality signal nor explainable infeasibility.

  • The obvious way to encode this problem produces 691.2 million boolean variables and does not solve. Modelling it as one integer variable per lesson for slot, teacher and room — with channelling variables where the constraints need them — brings it to roughly 108,000 booleans. That is a 6,400-fold reduction, and it is the entire reason a ten-minute budget is plausible rather than fantasy.

  • The grid is sized at six working days by eight periods, not five by eight, because the default weekend here is Friday and Saturday and an institution that teaches on Saturday sets it differently. Sizing on the five-day case and discovering the six-day case in production is a specific failure this decision exists to prevent.

  • When a routine is impossible, the solver says which constraints make it impossible. Every relaxable constraint sits behind an assumption literal, so an infeasible model yields a minimal core that can be translated into a sentence: this class needs five periods a week, the only competent teacher is unavailable on two days, and here are the three things that would change it. A solver that says no without saying why is worse than the spreadsheet it replaced.

  • After a single change — one teacher’s availability, one room withdrawn — at least 90% of unchanged lessons must keep their slot, teacher and room. That is a hard requirement, checked in the build. A re-solve that moves four hundred lessons and reprints seventy teachers’ cards to accommodate one absence is technically correct and commercially fatal.

  • The same input with the same seed produces the same routine, every time. Parallel search is only deterministic when the worker count is fixed and interleaved search is on, so both are pinned — and so is the solver version, because upgrading it rewrites routines that nobody asked to change.

Variable encoding, 2,000 lessonsSpecification
The obvious encoding691.2 millionOne boolean per lesson, slot, teacher and room: 2,000 × 48 × 120 × 60. Does not solve.
One integer per lesson for slot, teacher and room≈ 108,000Booleans, with 96,000 channelling variables for slots and about 12,000 for teacher assignment.
  1. First feasible routineWithin 60 secondsImproving answers stream to the screen.
  2. Full solveWithin 10 minutesThe budget for 2,000 lessons, 120 teachers and 60 rooms.
  3. If infeasibleA named set of constraints to relaxWritten in Bangla and English.
Six days by eight periods is 48 slots120 teachers · 60 rooms

Specification, not measurement. The ten-minute solve at full size rests on this arithmetic and is still to be confirmed by an experiment; the reduction is why that budget is plausible, not a guarantee.

Seventy-two hours with no network, and nothing lost

A teacher marks attendance in a classroom with no Wi-Fi. A field officer records mortality inside a poultry shed with no signal at all, one-handed, in about forty seconds.

  • No screen in a capture flow makes a network call — including validation. Every piece of reference data the flow needs, from cause codes to medication catalogues to species standards, lives on the device.

  • There are two mechanisms, deliberately, because there are two jobs. The attendance app carries a hand-rolled append-only queue over browser storage with exactly one write shape, because it must not drag a sync SDK into a 250 KB budget. A unit test asserts that the queue’s type union has exactly one member, so adding a second write shape is a reviewed decision rather than a drift.

  • The field app carries a full sync engine with nine write shapes, a local database and deterministic conflict rules, because it holds a full route, batch state and historic visits and has to survive three days.

  • Sync conflicts resolve by rule, not by last-writer-wins. Last-writer-wins silently discards a record somebody drove to a farm to collect.

Two clients, two mechanismsSpecification
Attendance app · web
Write shapes1
Offline read set≈ 40 KB
Bundle budget250 KB gzipped
QueueHand-rolled, append-only

A sync SDK does not fit the budget, and a unit test asserts the queue has exactly one write shape.

Field app · Android
Write shapes9
Offline read set≈ 2 to 6 MB
Bundle budgetNone, installed
Offline duration72 hours

A full sync engine, a local database and deterministic conflict rules, because it holds a full route and has to survive three days.

Conflict rule, by kind of record
  • Append-only eventsNo conflict is possible. Every capture is a new row keyed by a client mutation id, and duplicates are deduplicated, never merged.
  • Measurements with a natural keyLast writer wins by capture time, and the loser is kept, so a dispute is answerable.
  • Server-authoritative recordsThe server wins. The client change is rejected with a reason and shown in the sync queue.
  • One writer per scopeFirst writer wins. Two officers cannot both close a batch.
  • MoneyNever captured offline. The field app cannot originate a money movement.
  • CorrectionsA new row that references the original, with a reason. After the correction window, a supervisor role is required.
Conflict rules live in the schemaNo loss · no duplicate · no silent overwrite

Specification, not measurement. Both clients are designed to these rules; neither is deployed. Offline duration for the attendance app is one school day; for the field app, 72 hours.

Money that reconciles, in a country with its own rails

A guardian pays through a mobile wallet, or at a bank counter, or in cash at the school office. Three days later a settlement file arrives and somebody has to make all of it agree.

  • Reconciliation is three-way: the invoice, the gateway’s record of the attempt, and the settlement line the bank actually paid. Anything that does not match lands in an exceptions queue under one of nine codes, in one of four work queues, with an age on it.

  • Matching is exact, never probabilistic. A fuzzy match that silently attaches a payment to the wrong student is worse than a queue somebody has to work, because the first one is discovered by a parent and the second by a clerk.

  • Payment gateways sit behind an adapter with at least two implementations plus a manual method, because the commercial terms of any one gateway are not a thing to build an architecture around.

  • The institution is merchant of record and Nimikh is never in the flow of funds. That is not a design preference — aggregating payments in Bangladesh is a licensed activity with a capital requirement and personal liability attached.

Funds path and three-way matchSpecification
Where the money goes
  1. GuardianPays by wallet, bank counter or cash
  2. Licensed gatewayThe institution’s own account
  3. Institution’s bankMerchant of record
NimikhStores references and reconciliation state. Never holds, routes or settles funds.
What is compared
InvoiceWhat we asked for
Gateway transactionWhat the provider says happened
Settlement lineWhat the bank actually received
Matches automatically
  • CertainProvider transaction id and gross amount agree.
  • CertainPayment reference and amount agree, with exactly one candidate.
  • HighSingle candidate on amount, date and channel, flagged for the daily digest.
No rule below high confidence matches automatically.
Everything else: a person decides
  • Needs action
  • Waiting on gateway
  • Waiting on tenant
  • Resolved, 30 days
Every manual match records who matched it and why. The queue follows from the exception code; nobody sets it by hand.
Exact match, or a human decidesNine exception codes · four queues

A designed reconciliation flow, not a customer ledger. No payment has been processed through it, and the settlement file layout is still to be confirmed against a real merchant account.

Isolation enforced by the database, proved by a test

One deployment holds many institutions’ student records, staff records and money. A bug that crosses that boundary is not a bug, it is an incident.

  • The decisionDecision
    Chosen

    PostgreSQL, with row-level security as the isolation mechanism

    Because

    The isolation boundary and the money need to be the same transactional store, and row-level security makes the boundary a database object rather than a code convention.

    Rejected

    A schema per institution, which multiplies migration risk by the number of customers.

  • Every tenant-scoped table carries a tenant id with a row-level security policy. Application-layer checks are a convenience; the database is the control.

  • The whole API test suite runs a second time as a foreign tenant, and must return zero rows and zero successful responses on every single resource. A failure there blocks the release, unconditionally.

  • Roles are assigned per user per scope, not globally. A coordinator for the morning shift cannot dispatch for the day shift.

  • Support access into a customer’s data requires an open ticket, a consent flag, a reason code, a time box — and the customer is notified before the access happens, not after.

Five layers of tenant isolationSpecification
  1. L1
    EdgeThe host or subdomain gives a tenant hint. It is never trusted for authorisation.
    StopsNothing on its own
  2. L2
    SessionThe session carries the user. Tenant memberships are resolved on the server from role assignments.
    StopsA forged tenant header
  3. L3
    RequestMiddleware resolves the effective tenant and asserts membership before any handler runs.
    StopsA cross-tenant path, such as another tenant’s students endpoint
  4. L4
    TransactionThe tenant, user and role are set locally at the start of every database transaction.
    StopsA repository that forgets its tenant filter
  5. L5
    DatabaseA row-level security policy on every tenant table, forced, and the application role cannot bypass it.
    StopsAny SQL injection or logic bug in the four layers above
The foreign-tenant testFor every endpoint in the generated API document, authenticate as another tenant at its highest role. Expect a refusal, never a success and never a server error, with no fixture data anywhere in the response body. Runs on every pull request and blocks the merge.
The test of the testRemove the forced policy from one table in a scratch database and the suite must go red. If it does not, the suite is not exercising layer 5.
The database is the last line of defenceLayer 5 is the control; the rest are conveniences

A designed isolation model, not an audit result. No penetration test or independent review has been carried out, and none is claimed.

Data that stays in the country, and leaves when you ask

Bangladesh’s Personal Data Protection Act has been in force since 15 April 2026 and imposes class-based localisation. There is no AWS, Azure or GCP region in the country.

  • In-country hosting here means colocation and a self-managed database, not a managed-service purchase. That is a real cost, and it is a cost a foreign vendor cannot avoid either.

  • Every record you gave us and every record we created for you, in open formats, self-serve, within fifteen minutes. Not a support ticket, not a data-liberation fee, not a quarterly export window.

  • One exception, stated here rather than in a footnote: biometric templates are not exported as templates, because they are non-portable by design and export-restricted. The export carries a signed inventory of what is held, and a purge certificate on request.

  • Every state change is attributed, timestamped and hash-chained. Nothing is silently changed, and the account owner can see all of it.

Residency postureSpecified
Primary databaseIn Bangladesh, colocated
StandbySecond facility in Bangladesh
BackupsImmutable ≥ 35 days
Export≤ 15 min, open formats
Audit trailHash-chained, append-only
Confidential data abroadNone, tested in the build
Personal Data Protection Act in force 15 April 2026Class-based localisation

Specification, not a hosting record. Nothing is deployed yet; this is where each class of data is specified to live.

Video that never leaves the site

Cloud video analytics is priced at USD 20–50 per camera per month. The thing it replaces costs BDT 10,000–15,000 a month and is a person.

  • Detection runs on a box at the site. Only events, metadata and short clips leave. The whole ingest and restream layer is permissively licensed, and so is the detector — embedding a model on a customer’s hardware is distribution, and a copyleft licence there would oblige publishing the product.

  • A detection is validated over time and filtered by class before anyone is told about it. An operator interrupted for nothing stops responding to the alerts that matter, which turns a detection system into an expensive way of doing nothing.

  • No accuracy figure, no false-alarm-reduction percentage and no camera-per-box count is published before it is measured on the customer’s own cameras. There is no audited figure for this class of system, and a number that comes from somebody else’s site is a number about somebody else’s site.

Video stays on siteSpecification
  1. On siteCamera streams on the local network
  2. Edge boxPermissively licensed ingest and detectorDetections are validated over time and filtered by class before anyone is told.
  3. Leaves the siteEvents and metadataA clip only when requested, over the site’s own outbound connection, to in-country storage.
The site polls; there is no inbound portNo accuracy or camera-count figure is published

A designed data path, not a deployment. VisionEdge is at the validation stage, and no site is running this.

Messaging with a consent ledger and a hard cap

A Bangla SMS is two or more parts at seventy Unicode characters each, and the parts are billed. Fee deadlines and result publication land in the same week.

  • Every send is priced before it is sent. A fee-due message is three parts in Bangla and two in English — the composer states that difference rather than quietly choosing the cheaper language on the reader’s behalf.

  • Messaging is prepaid into a per-institution wallet with a hard cap. Without the cap, one result day can consume an institution’s entire margin, and the institution would find out afterwards.

  • Consent is a ledger, not a checkbox. Who agreed to what, on which channel, when, and what they were told at the time.

Eight gates before a message is sentSpecification
  1. Gate 01Consent
  2. Gate 02Quiet hours
  3. Gate 03Wallet balance
  4. Gate 04Duplicate check
  5. Gate 05Channel choice
  6. Gate 06Render
  7. Gate 07Segments and cost
  8. Gate 08Dispatch
A suppressed send is recorded with its reasonVisible, never silent

A designed routing order from the technical requirements. No message has been sent through it.

Budgets set by the cheapest phone in the room

Around 30% of devices in Bangladesh run Android 12 or older (StatCounter, 31 Aug 2026), and the median connection is disputed enough that the budget is set at the low end of the range rather than the middle.

  • Key screens are interactive within three seconds on a four-gigabyte Android over five megabits with 200 ms of latency, with the JavaScript on teacher and guardian paths capped at 250 KB compressed. Exceeding it fails the build.

  • Blur filters, backdrop filters and multi-layer shadows are banned outright. They force a composite layer and measure roughly four times slower on the GPUs that dominate the low-cost phone segment — so the visual language is built from type, rule and space instead.

  • Only transform and opacity ever animate, at most two elements at once, and lists over twenty items do not animate at all. Motion explains a spatial relationship or points at a change the reader did not cause. It never signals success, quality or brand.

  • Availability is committed at 99.5% in the first year — which is the honest number for a single in-country primary — but the promise that matters is different: no change is deployed inside a peak window, and the daily dispatch window between 07:00 and 08:15 is named in the contract alongside fee deadlines and result days.

The stack, with the reason attached

Each card is one layer. The chosen line is the part every company publishes. The because and rejected lines are the part that can be argued with, and they are the only ones that tell you anything.

  • Language and runtimeDecision
    Chosen

    TypeScript on Node, strict

    Because

    One language across the web tier and the API, for a team small enough that a third runtime is a tax.

    Rejected

    Go, which would be a third runtime for a team of fewer than ten. Java with Spring.

  • WebDecision
    Chosen

    Next.js and React

    Because

    Guardian-facing surfaces are server-rendered and cacheable, because result day is a twenty-thousand-view burst against one endpoint and a client bundle cannot absorb it.

    Rejected

    A single-page app for the consoles, because two frontend build systems double the translation, sign-in and design-token surface. React Native for the first release, for install friction on shared low-end phones.

  • DatabaseDecision
    Chosen

    PostgreSQL

    Because

    Row-level security is the isolation mechanism, and money has to be atomic with the records it belongs to.

    Rejected

    MySQL, which has no comparable row-level security. MongoDB.

  • SolverDecision
    Chosen

    OR-Tools CP-SAT, isolated in its own service

    Because

    Anytime search, an optimality signal, and an infeasibility core that can be explained to a coordinator.

    Rejected

    Bespoke heuristics, which give no optimality signal and no explainable infeasibility. Commercial solvers.

  • Field applicationDecision
    Chosen

    React Native with a local database and a sync engine

    Because

    Background sync, camera and GPS, and three days of usable offline life on an Android handset.

    Rejected

    Realm and Atlas Device Sync, which reached end-of-life in September 2025. Flutter.

  • Object storageDecision
    Chosen

    Self-hosted, in-country, with an S3-compatible API

    Because

    The API keeps the application portable; the location satisfies the localisation requirement.

    Rejected

    MinIO. Cloudflare R2 for confidential classes, because it sits outside the country.

  • Edge videoDecision
    Chosen

    Permissively licensed ingest and detector on a commodity accelerator

    Because

    Putting a model on a customer’s hardware is distribution. A copyleft detector would oblige publishing the product.

    Rejected

    Ultralytics YOLO models, because an AGPL detector on a customer’s hardware would oblige publishing the product.

  • Error trackingDecision
    Chosen

    Self-hosted, inside Bangladesh

    Because

    An exception payload carries local variables and request bodies. That is confidential-class data, and a hosted tracker would carry it out of the country.

    Rejected

    Sentry as a hosted service, because it exports confidential-class data. Datadog, on cost.

Decisions recorded in the technical requirements. Rejected alternatives are listed with the reason they were rejected, not as a verdict on the products themselves.

How a decision like one of these gets made

Every number here is a commitment

Almost every figure in these specifications carries a threshold and a consequence attached to it — a build that fails, a release that is blocked, a drill that is timed. This is all of them in one place, for software that is still being built. Not one of them is a result measured on a service running for a customer, because there is no such service yet.

The numberWhat it commits to
300 ms and 800 msReads and writes respectively, at the ninety-fifth percentile, measured at the application edge. Held under load profiles run at twice the modelled peak, and at two and a half times before a known peak window.
3.0 secondsKey screens interactive on a four-gigabyte Android over five megabits with 200 ms of latency. Checked against that device profile in the build, not against the laptop the code was written on.
250 KB gzippedAll JavaScript on teacher and guardian paths. Exceeding it fails the build, which is why the attendance app carries a hand-rolled queue rather than a sync library.
125 KB, four filesThe entire font budget, both scripts included. Bangla faces are large, so they are subsetted to the glyph ranges actually used rather than shipped whole.
3 secondsFrom an absence being logged to substitute suggestions being on the screen, at the ninety-ninth percentile — inside the window where a coordinator is still holding the problem in their head.
60 seconds, then 10 minutesA first feasible routine, then the full solve, for 2,000 lessons across 120 teachers and 60 rooms. The first number is the one that matters in practice: improving answers stream to the screen rather than a progress bar spinning against a silent process.
90% of unchanged lessonsAfter a single change — one teacher’s availability, one room withdrawn — at least this share keep their slot, their teacher and their room. The test is blocking, and the diff is shown on screen before a routine is published.
2,500 invoices in 60 secondsIssued asynchronously with progress shown, rather than a screen that appears to have frozen while a term’s billing generates.
72 hoursCapture with no network at all, syncing back with no loss, no duplicate and no silent overwrite, and every conflict visible in the queue rather than resolved out of sight.
15 minutesA complete self-serve export for a 2,500-student institution — every record you gave us and every record we created for you, in open formats. Not a support ticket, not a fee, not a quarterly window.
15 minutes and 4 hoursRecovery point and recovery time, with a timed restore drill every month. A recovery objective nobody has rehearsed is a number rather than a plan.
35 daysThe minimum period a backup stays immutable. Encrypted, and held in a second facility inside Bangladesh, so a bad actor with production access still cannot erase the history.
20,000 views in 10 minutesGuardian page views against a single endpoint on result day, tested cold-cache as well as warm. A burst that only passes warm is not a tested burst.
15 minutesThe time within which a recording failure or a device health failure is detected. A camera that quietly stopped recording last Tuesday is worse than no camera, because somebody believed it was working.
99.5% monthlyAvailability in the first year. That is the honest figure for a single in-country primary with a manual-promotion standby, and 99.9% was rejected with the arithmetic rather than adopted as a slogan.
07:00 to 08:15The daily dispatch window, named in the contract alongside fee deadlines and result days. No change is deployed inside it, and none inside the others.
WCAG 2.2 AAAutomated checks on every end-to-end page plus a manual keyboard pass on every screen rather than a sample, blocking the release on a violation. A functional constraint, not a badge — there is no certificate, and accessibility certificates do not exist.
44 × 44 pxEvery touch target, asserted in component tests, so a control that shrinks below it fails before anyone tries to tap it one-handed on a bus.
360 pxThe narrowest supported width, treated as the design floor rather than a fallback. A visual check catches Bangla overflow there, because Bangla sets 20 to 30 per cent longer than the English a layout was drawn against.

Commitments, not results. Several still depend on an experiment we have not run. The ten-minute solve envelope at full school size is the clearest case: it rests on the arithmetic of the encoding, not on a stopwatch.

The same discipline governs what is missing. There is no uptime history, no certification, no penetration-test result and no accuracy figure anywhere on this page, because none of those has happened. Where a figure would have to come from work we have not yet done, it is absent rather than estimated.

Rejected target

The target we refused

A blanket ninety-fifth percentile under 200 ms was proposed for every endpoint, and rejected. A reconciliation matching endpoint and a batch of two and a half thousand invoices cannot meet it, and a target a team cannot meet is a target a team learns to measure around — by excluding the slow endpoints, by sampling the quiet hour, by redefining what counts as a request. Three hundred milliseconds for reads and eight hundred for writes hold across every endpoint, including the awkward ones. Which target somebody refused tells you more than which one they accepted.

Degraded mode

The promise we cannot make

Recovery time is committed at four hours. The dispatch window is forty-five minutes long. Those two numbers do not reconcile, and we are not going to pretend they do: if the service is down at 07:10, we will not be back before the bell. So the commitment is a different one — dispatch degrades rather than stops. The last published routine stays readable offline, on the printed cards and on the front-desk board. Absences come in by SMS. And when the service returns, every substitution is recorded with the time it actually happened rather than the time somebody got round to typing it.

Start here

Argue with any of it

If one of these decisions is wrong for your situation, the reason is written down and can be tested against your constraints rather than ours. Tell us what the system has to survive and we will tell you which of the above changes.