Skip to content

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:

text
  ┌──────────────┐
  │ 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:

sql
ALTER TABLE students ADD COLUMN new_field_name TEXT;

For a NOT NULL column with a default:

sql
ALTER TABLE students ADD COLUMN new_field_name TEXT NOT NULL DEFAULT '';

For a boolean:

sql
ALTER TABLE students ADD COLUMN new_flag INTEGER NOT NULL DEFAULT 0;

For a foreign key:

sql
ALTER TABLE students ADD COLUMN other_id TEXT REFERENCES other_table(id);

Apply locally with:

bash
pnpm db:migrate:local

Step 2 — Worker Zod Schemas

File: workers/src/schemas.ts

Add the field to studentCreateSchema:

typescript
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:

typescript
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:

typescript
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:

typescript
{ key: "newFieldName", labelKey: "students.newFieldName", type: "string", required: false, maxLength: 200 },

For a required field:

typescript
{ key: "newFieldName", labelKey: "students.newFieldName", type: "string", required: true, maxLength: 200 },

For a boolean:

typescript
{ key: "newFlag", labelKey: "students.newFlag", type: "boolean", required: false },

For a date:

typescript
{ 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:

typescript
{ 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

  1. Add the field to the AddStudentInput interface.
  2. Add it to the buildChangePayload tracked fields array.
  3. Include it in the db.students.add(...) call.
  4. Add it to UpdateStudentInput and 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:

typescript
students: {
  // ... existing keys ...
  newFieldName: "Nome do novo campo",
}

6b. Form + Profile

File: app/src/app/pages/StudentsPage.tsx

  1. Add a new state variable for the form value.
  2. Add the input field in the Add/Edit dialog form.
  3. Add the field display in the profile dialog (the profileStudent section).
  4. Wire it through handleAddStudent and handleEditStudent.

Example: Adding a "nickname" field

Let's walk through adding an optional nickname field end-to-end.

Migration (migrations/0004_nickname.sql)

sql
ALTER TABLE students ADD COLUMN nickname TEXT;

Schema (workers/src/schemas.ts)

typescript
export const studentCreateSchema = z.object({
  // ... existing ...
  nickname: z.string().max(100).optional(),
});

Types (app/src/db/types.ts)

typescript
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)

typescript
nickname: "Apelido",

UI (app/src/app/pages/StudentsPage.tsx)

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 TypeD1 ColumnZod SchemaTS Type
Required textTEXT NOT NULLz.string().min(1).max(N)string
Optional textTEXTz.string().max(N).optional()string | undefined
BooleanINTEGER NOT NULL DEFAULT 0z.boolean()boolean
Date (YYYY-MM-DD)TEXTz.string().regex(/^\d{4}-\d{2}-\d{2}$/)string | undefined
JSON arrayTEXT NOT NULL DEFAULT '[]'z.array(someSchema)SomeType[]
Foreign keyTEXT REFERENCES other(id)z.string().uuid()string | undefined
EnumTEXTz.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

Distribuído sob licença MIT.