Family Center — Technical Reference
Where this lives
Backend
apps/backend/graphql/types/family-supervision.type.js—FamilySupervision,ChildTimeLimitResult,ChildUsageStats/ChildUsageDaytypes, and the query/mutation fields below. Reuses the globalUsertype andDateTimescalar declared elsewhere.apps/backend/graphql/resolvers/family-supervision.resolver.js— thin auth-check + delegate to the manager, no explicit registration needed (graphql/resolvers.js/typeDefs.jsauto-discover every file under their directories vialoadFilesSync).apps/backend/managers/user-managers/family-supervision.manager.js— all business logic: mutual-consent invite/accept/decline, either-party end, and parent-only authorization for the child's screen-time data. Delegates the screen-time limit itself tousage-tracking.manager.js(the existing "Time management" feature,user.settings.time_managementJSONB) rather than storing a duplicate copy — this manager only adds the authorization check that the caller is the active parent on the link before reading/writing someone else's settings.apps/backend/data-access-services/user/family-supervision.access-service.js— Sequelize wrapper around thefamily_supervisiontable.apps/backend/database/models/FamilySupervision.js/apps/backend/database/migrations/20260802130000-create-family-supervision.js— creates thefamily_supervisiontable (see Data model below). Runnpx sequelize-cli db:migrateafter pulling if you haven't already — this migration is not automatically applied to every environment.
Frontend — apps/frontend-nextjs/src/page-components/settings/FamilyCenterPage.tsx, routed at Settings → Family Center (app/settings/family-center/page.tsx, linked from the "App and media" section of the settings nav). Uses @closegram/apollo-web/operations generated documents from packages/graphql/operations/Web/Family/*.graphql — run npm run build -w @closegram/apollo-web after pulling if you see missing-export errors on these imports; that build step (which runs codegen) needs to be re-run any time new .graphql operation files are added, and isn't automatic.
Technical implementation checklist
-
requestFamilySupervision(targetUsername, requestingUserRole)— either user can invite;requestingUserRole('parent' | 'child') is which role the caller wants to play. Rejects self-invites and a duplicate pending/active link between the same pair (checked in either direction). Sends afamily_supervision_invitenotification to the invited user. -
respondToFamilySupervision(linkId, accept)— only the invited party (not the requester) may respond, and only while the link is stillpending. Notifies the requester of the outcome. -
endFamilySupervision(linkId)— either party may end an active link or cancel a pending one, at any time (a deliberate product decision — supervision is always revocable by both sides, not just the parent). Idempotent for an already-ended/declined link. Notifies the other party only when ending a link that was actuallyactive. -
mySupervisionAsParent/mySupervisionAsChild— links where the caller is the parent/child respectively, any status, newest first. -
setChildTimeLimit(linkId, minutes)— caller must be the active parent onlinkId; delegates tousage-tracking.manager.js#updateTimeManagementSettings. Passminutes: nullto clear the limit. -
childUsageStats(linkId, days)— read-only, active-parent-only; mergesusage-tracking.manager.js#getUsageStatswith the currentdailyLimitMinutes.
GraphQL reference
type FamilySupervision {
id: ID!
parent: User
child: User
status: String! # pending | active | declined | ended
requestedByUserId: ID!
respondedAt: DateTime
endedAt: DateTime
createdAt: DateTime!
updatedAt: DateTime
}
query MySupervision {
mySupervisionAsParent { id status child { id username } }
mySupervisionAsChild { id status parent { id username } }
}
mutation RequestSupervision($targetUsername: String!, $requestingUserRole: String!) {
requestFamilySupervision(targetUsername: $targetUsername, requestingUserRole: $requestingUserRole) {
id status
}
}
mutation RespondToSupervision($linkId: ID!, $accept: Boolean!) {
respondToFamilySupervision(linkId: $linkId, accept: $accept) { id status }
}
mutation EndSupervision($linkId: ID!) {
endFamilySupervision(linkId: $linkId) { id status endedAt }
}
mutation SetLimit($linkId: ID!, $minutes: Int) {
setChildTimeLimit(linkId: $linkId, minutes: $minutes) {
linkId childUserId dailyLimitMinutes
}
}
query ChildUsage($linkId: ID!, $days: Int) {
childUsageStats(linkId: $linkId, days: $days) {
linkId userId days dailyAverageSeconds todaySeconds dailyLimitMinutes
series { date totalSeconds }
}
}
Data model
The family_supervision table holds one row per link (mutual-consent, either direction):
| Column | Type | Notes |
|---|---|---|
id | UUID (PK) | |
parent_user_id, child_user_id | UUID | which side is "the parent" is decided at invite time by requestSupervision, not fixed by who initiated |
status | STRING(20) | pending (default) → active (accepted) / declined, or active → ended |
requested_by_user_id | UUID | which of the two users sent the invite — the other one must respond |
responded_at, ended_at | DATE | nullable |
ended_by_user_id | UUID | nullable |
created_at, updated_at | DATE |
Indexed on parent_user_id, child_user_id, and (parent_user_id, child_user_id, status) (speeds up the duplicate-invite guard).
The screen-time limit itself is not stored on this table — it lives on the child's own user.settings.time_management JSONB (see the Time management feature), and this feature only adds the authorization check that gates the linked parent reading or writing it.