Your data model is data, not code.
Most healthcare software hard-codes its schema. Change a form and you change the codebase, which means a release, a migration and a wait. CareSewa reads your models at request time, from documents you control. Add a field, and the form, the table, the validator and the API all change together. Immediately.
- 18
- field types
- text to relation
- 1
- record collection
- every model, every tenant
- 0
- migrations
- to add a model
- 0s
- to roll out
- next render, not next release
One extra field. Two very different weeks.
This is not a story about developers being slow. It is a story about an architecture that puts your schema on the wrong side of a build step.
Schema in the codebase
Six weeks
- 1You file a change request for one extra field
- 2It is scoped, quoted and scheduled into a release
- 3A developer edits a schema and writes a migration
- 4QA regression-tests a form that gained a text input
- 5It ships in six weeks, and by then the protocol moved
So the field lives in a spreadsheet instead, and the truth is now in two places.
Schema in the database
This afternoon
- 1You open Studio and add the field
- 2You set its type, label, order and whether it is required
- 3You save
- 4Your staff see it on their next render
- 5The API accepts it, the table shows it, the audit log records it
The truth stays in one place, because nothing had to route around the system.
Four ideas hold the whole engine up
None of them are clever. They are just applied consistently, which is the part most systems get wrong.
A field is a database record
Not a column, not a class property, not a line in a migration. It is a row inside a ModelDefinition document, and it can be created, reordered and removed like any other row.
Records live in one generic store
Every record of every model lands in DynamicRecord, keyed by tenant and model. Adding a model is an insert, not a schema change — which is why it takes a second instead of a sprint.
The UI is a function of the definition
DynamicForm, DynamicTable and DynamicPage take a definition and render it. Nobody hand-writes a form for your Patient model, so nobody has to hand-edit one when it changes.
The rules live in the engine
Validation, tenancy, permissions and audit are enforced where the write happens, not where the form is drawn. A curl request and a button click are the same request by the time it matters.
This is a model. That is all one is.
A key, a label, and a list of fields. Save it and the engine has everything it needs to validate writes, render forms and serve an API for it.
- modelKey is immutable once created — records are keyed by it
- Fields carry a type, a label, an order and their own rules
- Required, unique and select options are properties, not code
- A relation field names another model in your tenant
- The definition is scoped to one tenant and one portal
- Full nametextrequired
- Phonephoneunique
- Date of birthdate
- Blood groupselect4 options
- Primary doctorrelation→ doctor
- Consent formfile
Eighteen types. Enough for a hospital.
Not a generic form builder’s list. These are the shapes clinical and operational data actually takes, and each one carries its own validation into every write.
Text
Short lines, long notes, and the difference between them.
Numbers
Currency is stored as a number and formatted per market — never with a baked-in symbol.
Time
Dates of birth, appointment slots, and shift start times are three different problems.
Contact
Validated on write, not just masked in the input.
Choice
Options are part of the definition, so changing them changes every form at once.
Structure
Relation points at another model in your tenant. JSON is the escape hatch for shapes nobody predicted.
All 18, in the engine’s own order: text · string · number · integer · currency · boolean · date · datetime · email · phone · url · select · multiselect · relation · file · json · textarea · time
What happens between you and the record
Six steps, and you only author the first one. Everything after it is the engine doing what your definition told it to.
You define the model
One ModelDefinition — a key, a label, fields with types and rules. The only authoring step.
A write arrives
From a form, the app, or a curl against /data/:modelKey. All three land in one handler.
The engine validates it
Against your definition: types, required, options, uniqueness, and relation targets in your tenant.
The record is stored
Into DynamicRecord, stamped with your tenantId, and written to the append-only audit log.
The API already exists
List, read, create, update, delete — live the moment you saved. No endpoint was written.
The UI already exists
Forms and tables render from the same definition. Staff, app and integrations all see it.
There is no code generation step in this flow. That is deliberate. Generated code has to be regenerated, reviewed and redeployed — which is the exact problem we were removing.
Add a field. Watch the form change.
This is the loop, running in your browser. Add, reorder or remove on the left; the form your staff would use re-renders on the right. Nothing here is a video.
Model structure
4 fields- Full nametextreq
- Phonephonereq
- Date of birthdate
- Blood groupselect
Add a field
In the real Studio this saves to your ModelDefinition and every user sees it on their next render. No deploy, no migration.
Dynamic is not the same as loose
Every rule you set in the definition is enforced on the server, on every write, from every source. The form is a convenience. The engine is the authority.
Relations resolve inside your tenant
A relation field points at another model you defined. On write, the engine checks the target record exists — and that it belongs to your tenant. A record cannot reference something it should not be able to see.
Uniqueness is enforced, not suggested
Mark a field unique and the engine rejects the duplicate at the write, scoped to your tenant and model. Two hospitals can both have a patient with the same national ID. One hospital cannot have them twice.
Type and required checked server-side
A date field will not take a sentence. A required field will not take an empty string. Bypassing the UI and calling the API directly gets you the same 422 with the same field errors.
Select options are part of the contract
Options live on the field, so the form, the API and the validator agree by construction. Change the list and every surface changes with it — including the one your integration is writing to.
Isolation you cannot forget to apply
Tenancy is not a checkbox on a settings page. It is a field on every document and a filter on every query, which is the only version of it that survives contact with a growing codebase.
- Every ModelDefinition carries a tenantId
- Every DynamicRecord carries a tenantId
- Every query filters by it before anything else runs
- Relations are resolved within the tenant, never across
- Cross-tenant reads require an explicit super-admin assertion
- Two tenants can define the same modelKey and never meet
One collection · three tenants
find({ tenantId, modelKey }) — the tenant filter is not optional, and there is no query path that omits it.
Thirteen roles, and grants down to the model
Roles decide what kind of user someone is. Grants decide what they can touch. Both are read from the database on every request, which is what makes revocation mean something.
A staff grant, in full
You pick the portals a person can enter, then the models inside them, then the rights on each model. Nothing is inherited by accident.
This technician can log samples and read the catalog. They cannot delete anything, and they cannot see the technician directory at all — the model is not in their grant, so it is not in their navigation either.
13 roles
From account owner to portal staff to patient. Your role decides which surfaces exist for you at all.
BUILDER_ROLES edit models
Only builder roles can change a ModelDefinition. Staff use the models; they do not reshape them.
Read fresh, every request
Grants are not baked into a token at login. They are read from the database each request, so revoking access takes effect on the next one — not at the next sign-in.
Every mutation, written down and never rewritten
Clinical data attracts questions months later. The audit log exists so the answer is a query rather than an argument.
- Written on every create, update and delete
- Records the actor, the action, the model and the time
- Append-only — there is no edit path, for anyone
- Tenant-scoped like everything else
- Soft delete keeps medical records recoverable
- Removing a field stops collection, it does not erase history
- updatepatientr.sharma · nurse09:42
- createsampleapi · analyser-bridge09:41
- updatemodel:patienta.khan · admin09:38
- deleteappointmentfront-desk-209:31
- createreportl.tan · technician09:24
Model edits are audited too. Changing the shape of your data is itself a mutation.
Three kinds of change, three very different waits
Most of what you want to change is the first kind, and the first kind has no wait at all. That is the whole design goal.
Schema push
You change a model. The definition updates and every surface reads the new one on its next render — web and mobile alike, because both render from the same definitions.
No build. No store. No version skew.
Over-the-air update
You change the app’s JavaScript — a screen, a flow, a fix. EAS Update publishes it and devices pick it up on next launch.
No app-store review in the path.
Native build
Only when native code changes — a new permission, a new SDK. This is the slow path, and it is the one we deliberately keep rare.
Rare by design, not by luck.
Define a model, get an API
Not as a feature you enable. As a consequence of how the engine reads your definition. If the model exists, the endpoint exists.
Every model you build is reachable at /api/v1/data/:modelKey with list, read, create, update and delete. Same auth, same envelope, same validation, same audit log as the UI — because it is the same code underneath.
- GET/api/v1/data/patientlist, paginated
- POST/api/v1/data/patientcreate, validated
- GET/api/v1/data/patient/:idread one
- PATCH/api/v1/data/patient/:idupdate, audited
- DELETE/api/v1/data/patient/:idsoft delete
Swap patient for any model you defined. It was never a special case.
What the engine does not do
Every platform has edges. Ours are worth knowing before you plan around them, not after.
A modelKey is forever
Records are keyed by it, so it cannot be renamed after creation. Labels change freely; the key does not. Pick it once, and pick it carefully.
It is not a code generator
You will not find a generated repo to fork. If your requirement genuinely needs bespoke server logic, that is a conversation with us, not a Studio setting.
JSON is the last resort
The json field type holds shapes the other seventeen cannot. It is a real escape hatch, but the engine cannot validate inside it — so use it when you mean it.
What engineers ask about the engine
The architecture questions, answered the way we would answer them in a technical review.
No. There is no generated code to escape to. A model is a document in the database, and the application reads it at request time. Nothing is compiled, nothing is scaffolded, and there is no file on disk that has to agree with your schema.
See the engine against your own workflow
Bring the form your vendor said would take six weeks. We will model it on the call.
Multi-country by design · tenant-isolated · every change audit-logged