How to Add a New Field to the Student Entity
This guide documents the repeatable 5-step pattern for adding any field to the student record. Follow these steps in order — every layer must be updated for the field to work end-to-end.
Philosophy
Neemias follows an Open Core architecture: the schema is the source of truth, and every layer maps cleanly to it. Adding a field is a mechanical, predictable process — no surprises, no hidden coupling.
The 6 layers you must touch:
┌──────────────┐
│ 1. D1 Schema │ migrations/????_*.sql
├──────────────┤
│ 2. API Types │ workers/src/schemas.ts
├──────────────┤
│ 3. Frontend │ app/src/db/types.ts + db/db.ts
│ Types │
├──────────────┤
│ 4. Manifest │ app/src/modules/importExport/fieldManifest.ts
├──────────────┤
│ 5. Services │ studentService.ts + backendSync.ts
├──────────────┤
│ 6. UI │ StudentsPage.tsx + i18n.ts
└──────────────┘Step 1 — D1 Migration
Create a new file migrations/XXXX_descriptive_name.sql.
For a simple column:
ALTER TABLE students ADD COLUMN new_field_name TEXT;For a NOT NULL column with a default:
ALTER TABLE students ADD COLUMN new_field_name TEXT NOT NULL DEFAULT '';For a boolean:
ALTER TABLE students ADD COLUMN new_flag INTEGER NOT NULL DEFAULT 0;For a foreign key:
ALTER TABLE students ADD COLUMN other_id TEXT REFERENCES other_table(id);Apply locally with:
pnpm db:migrate:localStep 2 — Worker Zod Schemas
File: workers/src/schemas.ts
Add the field to studentCreateSchema:
export const studentCreateSchema = z.object({
// ... existing fields ...
newFieldName: z.string().min(1).max(200), // required
// OR
newFieldName: z.string().max(200).optional(), // optional
// OR
newFlag: z.boolean(), // boolean
});If the field should be updatable, it's already covered — studentUpdateSchema is studentCreateSchema.partial().
Step 3 — Frontend Types + Dexie
3a. TypeScript interface
File: app/src/db/types.ts
Add the field to the Student interface:
export interface Student {
// ... existing fields ...
newFieldName?: string; // optional
// OR
newFieldName: string; // required (update seed + all construction sites)
}3b. Dexie schema (only if you need a new index)
File: app/src/db/db.ts
Add an index ONLY if you'll query/sort by this field:
this.version(N).stores({
students: "studentId, status, displayName, classId, newFieldName",
// ...
});Step 4 — Field Manifest (Import/Export)
File: app/src/modules/importExport/fieldManifest.ts
Add a new entry to STUDENT_FIELD_MANIFEST for the field. This ensures the field appears in CSV/JSON exports, import validation, and downloadable templates.
For a simple text field:
{ key: "newFieldName", labelKey: "students.newFieldName", type: "string", required: false, maxLength: 200 },For a required field:
{ key: "newFieldName", labelKey: "students.newFieldName", type: "string", required: true, maxLength: 200 },For a boolean:
{ key: "newFlag", labelKey: "students.newFlag", type: "boolean", required: false },For a date:
{ key: "newDate", labelKey: "students.newDate", type: "date", required: false },If the field is part of a nested object (like phones or address), add CSV flattening columns:
{ key: "newFieldName", labelKey: "students.newFieldName", type: "string", required: false, parent: "parentObject", csvColumn: "newFieldCsv" },Important: The labelKey must match an existing i18n key (added in Step 6). The key must match the property name in the Student interface (added in Step 3).
Step 5 — Services
5a. Student service
File: app/src/modules/students/studentService.ts
- Add the field to the
AddStudentInputinterface. - Add it to the
buildChangePayloadtracked fields array. - Include it in the
db.students.add(...)call. - Add it to
UpdateStudentInputand the update handler.
5b. Sync engine
File: app/src/modules/sync/backendSync.ts
Add the field to the resolvePayload function in both the CREATE and UPDATE branches so offline-created events carry the field when syncing.
Step 6 — UI
6a. Translation keys
File: app/src/app/utils/i18n.ts
Add labels in the students section:
students: {
// ... existing keys ...
newFieldName: "Nome do novo campo",
}6b. Form + Profile
File: app/src/app/pages/StudentsPage.tsx
- Add a new state variable for the form value.
- Add the input field in the Add/Edit dialog form.
- Add the field display in the profile dialog (the
profileStudentsection). - Wire it through
handleAddStudentandhandleEditStudent.
Example: Adding a "nickname" field
Let's walk through adding an optional nickname field end-to-end.
Migration (migrations/0004_nickname.sql)
ALTER TABLE students ADD COLUMN nickname TEXT;Schema (workers/src/schemas.ts)
export const studentCreateSchema = z.object({
// ... existing ...
nickname: z.string().max(100).optional(),
});Types (app/src/db/types.ts)
export interface Student {
// ... existing ...
nickname?: string;
}Service (app/src/modules/students/studentService.ts)
Add nickname to the AddStudentInput interface, the buildChangePayload fields, the db.students.add() call, and the UpdateStudentInput + update handler.
Sync (app/src/modules/sync/backendSync.ts)
Add nickname to the CREATE payload and UPDATE field list.
i18n (app/src/app/utils/i18n.ts)
nickname: "Apelido",UI (app/src/app/pages/StudentsPage.tsx)
const [nickname, setNickname] = useState("");
// In the form:
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">{t.students.nickname}</label>
<input
value={nickname}
onChange={(e) => setNickname(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/>
</div>;Quick Reference: Field Type Patterns
| Desired Type | D1 Column | Zod Schema | TS Type |
|---|---|---|---|
| Required text | TEXT NOT NULL | z.string().min(1).max(N) | string |
| Optional text | TEXT | z.string().max(N).optional() | string | undefined |
| Boolean | INTEGER NOT NULL DEFAULT 0 | z.boolean() | boolean |
| Date (YYYY-MM-DD) | TEXT | z.string().regex(/^\d{4}-\d{2}-\d{2}$/) | string | undefined |
| JSON array | TEXT NOT NULL DEFAULT '[]' | z.array(someSchema) | SomeType[] |
| Foreign key | TEXT REFERENCES other(id) | z.string().uuid() | string | undefined |
| Enum | TEXT | z.enum([...]) | "A" | "B" | "C" |
Files Checklist
When you add a field, check off each file:
- [ ]
migrations/XXXX_*.sql - [ ]
workers/src/schemas.ts - [ ]
workers/src/router.ts(list/create/update/sync handlers +mapStudentRow) - [ ]
app/src/db/types.ts - [ ]
app/src/db/db.ts(only if index needed) - [ ]
app/src/modules/importExport/fieldManifest.ts - [ ]
app/src/modules/students/studentService.ts - [ ]
app/src/modules/sync/backendSync.ts - [ ]
app/src/app/utils/i18n.ts - [ ]
app/src/app/pages/StudentsPage.tsx - [ ]
app/src/db/seed.ts