This page connects a feature to the rest of the app: group settings, the overview, the inbox, a new account's first minutes and the public demo. Each section copies what notes or groups already do.
You need:
- The aggregate built (the aggregate reference).
A setting belongs to the group itself, such as its description, colour and emoji. It is not a new
aggregate: the group already has the table, the repository and the change event. Copy the details
command, group.updateDetails.
- Migration. Add the column, with a default or a nullable type so existing rows stay valid.
Add a
CHECKfor a limit the database can hold. Mirror it inlibs/server/db/schema.sql. - Domain. Add the field to
GroupandGroupSummaryinlibs/domain/groups/+lib.ts. Write one parser per field that throwsGroupErrorwithINVALID_REQUEST. Settings that change together share one command, so one request is one audit row. - Repository. Add the column to the selects,
toGroupandtoSummary. The write locks the member's row and checks the role. It then callsaudit()andrecordChange()in the same transaction. - Command and handler. The handler judges the role again (
assertCanEditDetails). Register the command inapps/api/cqrs/+init.ts,apps/api/index.tsandrealtimeHub.ts. - Transports. Add the route (
PUT /api/groups/:groupId/details) and the socket command, with the same bad-input and role tests as the other group commands. - Screen. Add the field to
libs/ui/group-editor-screen.tsxand to the create request. A role that may not edit sees the value as text.
A colour is a lower-case #rrggbb, checked with isHexColor from
@spy4x/platform/universal/color. Draw text on it with readableTextColor. New fields on
GroupRow, PickerGroup and the SPA's GroupItem are optional: the device may hold groups saved
before the field existed.
Other members need nothing new. recordChange() sends a hint to their sockets, and their pages
read the group again. e2e/groups-details.e2e.ts tests this with two pages.
The group overview (/overview/:groupId) is a page of numbers and charts about the group over a
range of days. Each aggregate offers its own widgets. A widget shows counts only, never an item's
content or who did it.
- Definition. Add the widget to the aggregate's list, as
NOTE_WIDGETSinlibs/domain/notes/overview.tsdoes. Its fields are in the table below. - Query and handler. Write one query class per widget and a handler, as
apps/api/features/notes/widgets.tsdoes. The handler makes the list's membership check before it reads anything, so a stranger is told the group does not exist. - Numbers. Read them from a read-only repository over one group, as
libs/server/notes/note-overview.tsdoes. Cut days at midnight in the person's zone. - Register. Add the handler to
apps/api/cqrs/+init.tsand the operation torealtimeHub.ts. - Drawing. Add a
WidgetView, asNOTE_WIDGET_VIEWSinlibs/ui/note-widgets.tsxdoes. A number takesWidgetStat; a chart takesLineChartwith a one-sentence summary as its caption. - Registry. Add the list and the drawings to
OVERVIEW_WIDGETSandOVERVIEW_VIEWSinlibs/ui/overview-widgets.ts. The SPA and the admin group page both read it. - Admin. Add the readers next to
NOTE_WIDGET_READERSinapps/api/routes/admin/groups.ts. Without them the admin card shows its error. - Tests. Copy
widgets.test.ts,tests/integration/note-overview.integration.test.tsand the drawing's case inlibs/ui/overview.test.tsx. Include another group's rows that must not count.
| Field | What it holds |
|---|---|
id |
A stable name. A saved layout keeps it, so it never changes |
titleKey |
The app catalog key of its name |
size |
stat for a number, full for a chart |
canView |
Which roles see it |
operation |
The operation that answers it |
A number answers with CountWidgetResult, and a chart with SeriesWidgetResult, from
@domain/groups. Both carry the period before the range, or null when the group did not exist
for all of it. A person's saved layout gets the new widget at its end.
A change that a person should hear about, and did not make, writes a notification. Copy
libs/server/groups/group-notifications.ts.
- Same transaction. Call
createNotification(tx, …)from@server/notificationsafter the change, inside its transaction. The row then exists exactly when the change does. - A kind. Add it to
NotificationKindinlibs/domain/notifications. Its sentence goes innotificationMessage, with its key in everyi18n/app/*.ts. - A payload and a link. The payload holds only the facts the sentence needs, never a note body or an address. The link is an in-app path; anything else is refused.
- A test. Add one per writer to
tests/integration/notifications.integration.test.ts.
Nothing else is needed. createNotification sends user_notification in the same transaction,
and the API turns it into a hint on the person's sockets. The inbox and Telegram say the same
sentence, in the person's language.
A new account should not open onto an empty app. Sign-up queues one worker job per account with
tx.starterData.queue(user.id). The job, starterDataJob in libs/server/jobs/starter-data.ts,
creates one welcome note. Sign-up never waits for it and never fails because of it.
- Add your data inside
starterDataJob, after the welcome note. Use your repository's own create, with the person as the actor. - Make the write repeatable. Derive each row's id from the request id, or use
ON CONFLICT DO NOTHING. The welcome note's id is the request's id, so a retry adds none. - Skip quietly when the group is gone: that means "nothing to seed", not a failure to retry.
- Test a forced retry in
tests/integration/starter-data.integration.test.ts: run the job twice, and count the rows.
The starter_data_requests row marks the job done, so a job that runs again does nothing. Every
list also needs a first-run empty state: EmptyState, with one action when there is something to
do (UI layout (on GitHub)).
A new account's home page shows a first-steps checklist and offers a short tour
(libs/ui/onboarding-card.tsx). Nothing ticks a step: each one is a fact your feature already
stores.
- The fact. Add one
EXISTS (…)toreadOnboardinginlibs/server/auth/onboarding.ts, and its name toOnboardingStepsinlibs/domain/identity/+lib.ts. Count only what the person did themselves, and give the read an index. - The step. Add it to
onboardingStepsin the card, with a link to the page that does it. - The tests. Add a step to
tests/integration/onboarding.integration.test.tsand its title tolibs/ui/onboarding.test.tsx.
To add a screen to the tour, add it to tourSteps with the selector of its control and its text
keys. Dismissing the card and finishing the tour are stored on the person's users row, so both
follow the person to every device.
A new aggregate adds fake rows to the demo in the same pull request (demo.md (on GitHub), "When the database changes").
- Add a few believable items per themed group to
apps/worker/demo/fake-data.ts. - Write them in
apps/worker/demo/seed.ts, through the aggregate's own repository. - Update the counts in
tests/integration/demo-seed.integration.test.ts.
The nightly wipe empties the new table with no change.
- Admin modules: the aggregate's admin counterpart.
- Delete, move and export an aggregate