Back to Equalify Dashboard

Database Schema

Edit on GitHub

Database Schema

Equalify uses PostgreSQL as its primary database, accessed via direct queries (serverless-postgres) and GraphQL (Hasura).

Core Tables

audits

Stores audit configurations and metadata.

ColumnTypeDescription
idUUIDPrimary key
user_idUUIDOwner's user ID (FK)
nameTEXTAudit display name
intervalTEXTScan frequency (manual, daily, weekly, etc.)
scheduled_atTIMESTAMPNext scheduled scan time
statusTEXTCurrent status (draft, new, processing, complete, failed)
payloadJSONBFull audit configuration
responseJSONBLatest scan response
email_notificationsBOOLEANEnable email alerts
created_atTIMESTAMPCreation timestamp
updated_atTIMESTAMPLast update timestamp

urls

URLs associated with audits for scanning.

ColumnTypeDescription
idUUIDPrimary key
user_idUUIDOwner's user ID (FK)
audit_idUUIDParent audit (FK)
urlTEXTFull URL to scan
typeTEXTContent type (html, pdf)
created_atTIMESTAMPCreation timestamp

scans

Individual scan runs for audits.

ColumnTypeDescription
idUUIDPrimary key
audit_idUUIDParent audit (FK)
statusTEXTScan status (processing, complete, failed)
percentageINTEGERProgress percentage (0-100)
pagesJSONBArray of pages to scan
processed_pagesJSONBArray of completed page IDs
errorsJSONBArray of scan errors
created_atTIMESTAMPScan start time
updated_atTIMESTAMPLast update timestamp

blockers

Individual accessibility issues found during scans.

ColumnTypeDescription
idUUIDPrimary key
scan_idUUIDParent scan (FK)
url_idUUIDURL where found (FK)
contentTEXTHTML snippet or context
content_hash_idUUIDHash for deduplication
equalifiedBOOLEANMarked as resolved
created_atTIMESTAMPDiscovery timestamp

messages

Accessibility rule definitions.

ColumnTypeDescription
idUUIDPrimary key
contentTEXTRule/message text
categoryTEXTMessage category
created_atTIMESTAMPCreation timestamp

blocker_messages

Junction table linking blockers to messages.

ColumnTypeDescription
idUUIDPrimary key
blocker_idUUIDBlocker reference (FK)
message_idUUIDMessage reference (FK)

tags

WCAG and other accessibility tags.

ColumnTypeDescription
idUUIDPrimary key
contentTEXTTag name (e.g., "wcag2aa")

message_tags

Junction table for message-tag relationships.

ColumnTypeDescription
idUUIDPrimary key
message_idUUIDMessage reference (FK)
tag_idUUIDTag reference (FK)

ignored_blockers

Blockers marked as ignored/resolved.

ColumnTypeDescription
idUUIDPrimary key
audit_idUUIDAudit reference (FK)
blocker_idUUIDBlocker reference (FK)
created_atTIMESTAMPWhen ignored

users

User accounts and profiles.

ColumnTypeDescription
idUUIDPrimary key (matches Cognito sub)
emailTEXTUser email address
nameTEXTDisplay name
roleTEXTUser role (user, admin)
team_idUUIDTeam membership (FK)
created_atTIMESTAMPAccount creation

teams

Organization/team groupings.

ColumnTypeDescription
idUUIDPrimary key
nameTEXTTeam name
created_atTIMESTAMPCreation timestamp

logs

Activity audit trail.

ColumnTypeDescription
idUUIDPrimary key
user_idUUIDActing user (FK)
actionTEXTAction type
detailsJSONBAction details
created_atTIMESTAMPAction timestamp

Relationships

teams
  └── users (many)
        └── audits (many)
              ├── urls (many)
              └── scans (many)
                    └── blockers (many)
                          └── blocker_messages (many)
                                └── messages (one)
                                      └── message_tags (many)
                                            └── tags (one)

Content Hashing

Blockers use content hashing for deduplication:

const contentHashId = hashStringToUuid(
  `${urlId}-${messageContent}-${normalizedNode}`
);

This allows:

  • Tracking blocker persistence across scans
  • Identifying when blockers are resolved
  • Preventing duplicate entries

GraphQL Access (Hasura)

Hasura provides GraphQL access with row-level security:

query GetAuditBlockers($audit_id: uuid!) {
  audits_by_pk(id: $audit_id) {
    scans(order_by: {created_at: desc}, limit: 1) {
      blockers {
        id
        content
        url_id
        blocker_messages {
          message {
            content
            category
            message_tags {
              tag {
                content
              }
            }
          }
        }
      }
    }
  }
}

Row-Level Security

Hasura enforces row-level security based on JWT claims:

{
  "x-hasura-allowed-roles": ["user", "admin"],
  "x-hasura-default-role": "user",
  "x-hasura-user-id": "user-uuid"
}

Users can only access:

  • Their own audits
  • Audits shared with their team
  • Admin access for team management

Atomic Operations

Scan progress uses atomic PostgreSQL operations to prevent race conditions:

UPDATE "scans" 
SET 
  "processed_pages" = CASE 
    WHEN NOT (COALESCE("processed_pages", '[]'::jsonb) @> $1::jsonb)
    THEN COALESCE("processed_pages", '[]'::jsonb) || $1::jsonb
    ELSE "processed_pages"
  END
WHERE "id" = $2
RETURNING "pages", "processed_pages"


For API usage examples, see the Backend API Reference.