Notifications and audit
User alerts, preferences, and accountability records
Notifications are user-facing work queues; audit logs are accountability records. A notification tells someone something happened. An audit log records that the application accepted an authentication, authorization, publishing, routing, or administrative action.
Notification
| Column | Prisma type | Required? | Default | Meaning |
|---|---|---|---|---|
id | String | Yes | cuid() | Notification identifier. |
userId | String | Yes | None | Recipient. |
kind | NotificationKind | Yes | None | Category used by the bell and settings. |
title | String | Yes | None | Short alert title. |
body | String | Yes | None | Human-readable details. |
linkUrl | String? | No | NULL | Optional in-app destination. |
read | Boolean | Yes | false | Whether the recipient marked it read. |
createdAt | DateTime | Yes | now() | Creation time. |
The user relation enforces recipient ownership. Notification list/read endpoints always filter by the current user's ID.
NotificationKind
| Value | Meaning |
|---|---|
SUBMISSION_MATCH | A submission matched a portfolio or interest profile. |
ANNOUNCEMENT | A government announcement was published. |
CALL_OPENED | A call for technology became available. |
ENGAGEMENT_UPDATE | An engagement-related update. |
SYSTEM | System or administrative notice. |
NotificationSetting reminder
| Column | Prisma type | Required? | Default | Meaning |
|---|---|---|---|---|
id | String | Yes | cuid() | Preference identifier. |
userId | String | Yes | None | Preference owner. |
kind | NotificationKind | Yes | None | Category. |
enabled | Boolean | Yes | true | On/off switch. |
techTags | String | Yes | "" | Optional event-tag filter. |
The unique pair (userId, kind) lets PUT settings safely upsert. A missing row means enabled by default.
AuditLog
| Column | Prisma type | Required? | Default | Meaning |
|---|---|---|---|---|
id | String | Yes | cuid() | Audit identifier. |
timestamp | DateTime | Yes | now() | Time of recorded event. |
event | String | Yes | None | Stable event label such as submission_routed. |
userEmail | String? | No | NULL | Actor or affected account email when available. |
ip | String? | No | NULL | Source IP when the route captures it. |
detail | String? | No | NULL | Optional JSON string with operational context. |
userAgent | String? | No | NULL | Request user-agent, truncated to 512 characters. Written by pageview rows. |
path | String? | No | NULL | Pathname of the recorded page, query string and fragment excluded. |
referrer | String? | No | NULL | Pathname portion of document.referrer, sent only on the first view after a full page load. The host is discarded, so an external referrer contributes its path only. |
Indexes: timestamp, event, and the composite (event, path) — the last one exists so the traffic summary can group pageviews by path without scanning the whole trail.
Audit logs intentionally do not use an enum: new operational events can be added without a migration. Current event labels include authentication_success, authentication_failure, authentication_rate_limited, authentication_locked, account_locked, authorization_failure, registration_rate_limited, registration_conflict, industry_registration, org_profile_viewed, org_profile_update, submission_created, submissions_viewed, submission_file_downloaded, submission_routed, submissions_rerouted, search_performed, notification_settings_updated, notifications_mark_read, announcement_published, call_opened, call_status_change, engagement_created, engagement_updated, engagement_deleted, engagement_note_created, org_dialogue_note_created, org_status_change, admin_commands_viewed, admin_command_viewed, admin_command_create, admin_command_update, admin_command_delete, admin_command_reparent, admin_users_viewed, admin_user_create, admin_user_update, admin_user_role_change, export_csv, export_all, export_ics, password_change_rate_limited, password_change_failure, password_change_success, password_change_blocked (a change attempted while credential changes are locked), and pageview.
Pageviews on the audit table
pageview rows share AuditLog rather than living in their own table: they are observational records with the same retention, access controls, and append-only semantics as the rest of the trail. They differ from other events in three ways — they are written by the client rather than by a mutating handler, they populate path/referrer/userAgent, and they are the only event read back in aggregate by a UI page.
Because the writer is a client effect, absence of a row is not evidence a page was not viewed: tracking is fire-and-forget, and a blocked request, an offline browser, or a rate-limited client simply produces no row. Treat pageview counts as usage signal, never as an authorization or accountability record — that is what the other event labels are for.
Notification flow
notifyUser, notifyUserForTags, notifyGovByTags, and notifyAllIndustry centralize fan-out policy. They create database rows rather than sending external email. This makes the in-app bell deterministic and auditable.
Why audit is not a notification
An audit record has an actor email, optional IP, event label, and operational detail. It is intended for reviewers and operators. A notification has a recipient, title, body, link, read state, and category. It is intended for a user's work queue. A successful action may produce both, only one, or neither depending on route purpose.
Audit detail is JSON serialized text in the current schema. Consumers should parse it defensively because event types do not share one fixed shape. The event label is the stable first filter; detail is supplementary context such as IDs, previous/new parents, match counts, or notification counts.
Audit rows are append-only by design: there is no soft-delete flag and no application path that edits or removes a row, so the trail only grows. Pair that with database access controls and a retention policy at the storage layer, which is where immutability is enforced for the long term.
