Skip to content
This repository was archived by the owner on Aug 11, 2026. It is now read-only.
This repository was archived by the owner on Aug 11, 2026. It is now read-only.

feat(visitor-analytics): add visitor session, event, and rollup schema with RLS #618

Description

@ahliweb

Context

Issue #617 adds the visitor_analytics module foundation. This issue adds the tenant-scoped PostgreSQL schema required to store visitor presence, visit events, and daily rollups for human visitor statistics.

AWCMS-Mini already enforces tenant safety using tenant_id, withTenant, ABAC, and PostgreSQL RLS. Visitor analytics must follow the same model and must not reuse audit tables as high-volume traffic tables.

Objective

Add visitor analytics database tables with RLS, FORCE RLS, indexes, and least-privilege grants.

Scope

Create a new migration using the next available migration number at implementation time. Do not hardcode an outdated migration number in the issue implementation.

Add these tables:

  • awcms_mini_visitor_sessions
  • awcms_mini_visit_events
  • awcms_mini_visitor_daily_rollups

awcms_mini_visitor_sessions

Suggested columns:

id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES awcms_mini_tenants (id),
visitor_key_hash text NOT NULL,
identity_id uuid NULL REFERENCES awcms_mini_identities (id),
login_identifier_snapshot text NULL,
is_authenticated boolean NOT NULL DEFAULT false,
area text NOT NULL,
current_path text NULL,
first_seen_at timestamptz NOT NULL DEFAULT now(),
last_seen_at timestamptz NOT NULL DEFAULT now(),
ip_hash text NULL,
ip_address inet NULL,
user_agent_hash text NULL,
browser_name text NULL,
browser_version_major text NULL,
os_name text NULL,
device_type text NULL,
is_human boolean NOT NULL DEFAULT true,
bot_reason text NULL,
country_code text NULL,
region text NULL,
city text NULL,
timezone text NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()

Recommended checks:

  • area IN ('admin', 'public', 'api', 'auth', 'setup', 'unknown')
  • device_type IN ('desktop', 'mobile', 'tablet', 'bot', 'unknown')

awcms_mini_visit_events

Suggested columns:

id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES awcms_mini_tenants (id),
visitor_session_id uuid NULL REFERENCES awcms_mini_visitor_sessions (id),
identity_id uuid NULL REFERENCES awcms_mini_identities (id),
occurred_at timestamptz NOT NULL DEFAULT now(),
method text NOT NULL,
status_code integer NULL,
area text NOT NULL,
route_pattern text NULL,
path_sanitized text NOT NULL,
referrer_domain text NULL,
duration_ms integer NULL,
ip_hash text NULL,
user_agent_hash text NULL,
user_agent_parsed jsonb NOT NULL DEFAULT '{}'::jsonb,
geo jsonb NOT NULL DEFAULT '{}'::jsonb,
human_status text NOT NULL,
correlation_id text NULL,
created_at timestamptz NOT NULL DEFAULT now()

Recommended checks:

  • area IN ('admin', 'public', 'api', 'auth', 'setup', 'unknown')
  • human_status IN ('human', 'bot', 'unknown')
  • status_code IS NULL OR (status_code >= 100 AND status_code <= 599)

awcms_mini_visitor_daily_rollups

Suggested columns:

tenant_id uuid NOT NULL REFERENCES awcms_mini_tenants (id),
date date NOT NULL,
area text NOT NULL,
human_unique_visitors integer NOT NULL DEFAULT 0,
human_pageviews integer NOT NULL DEFAULT 0,
bot_pageviews integer NOT NULL DEFAULT 0,
authenticated_unique_users integer NOT NULL DEFAULT 0,
public_unique_visitors integer NOT NULL DEFAULT 0,
admin_unique_users integer NOT NULL DEFAULT 0,
top_paths jsonb NOT NULL DEFAULT '[]'::jsonb,
top_browsers jsonb NOT NULL DEFAULT '[]'::jsonb,
top_devices jsonb NOT NULL DEFAULT '[]'::jsonb,
top_countries jsonb NOT NULL DEFAULT '[]'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (tenant_id, date, area)

Indexes:

CREATE INDEX ... ON awcms_mini_visitor_sessions (tenant_id, last_seen_at DESC);
CREATE INDEX ... ON awcms_mini_visitor_sessions (tenant_id, area, last_seen_at DESC);
CREATE INDEX ... ON awcms_mini_visit_events (tenant_id, occurred_at DESC);
CREATE INDEX ... ON awcms_mini_visit_events (tenant_id, area, occurred_at DESC);
CREATE INDEX ... ON awcms_mini_visit_events (tenant_id, human_status, occurred_at DESC);
CREATE INDEX ... ON awcms_mini_visit_events (visitor_session_id, occurred_at DESC);
CREATE INDEX ... ON awcms_mini_visit_events (identity_id, occurred_at DESC);
CREATE INDEX ... ON awcms_mini_visitor_daily_rollups (tenant_id, date, area);

RLS requirements:

  • Enable RLS on all three tables.
  • FORCE RLS on all three tables.
  • Policy: tenant_id = current_setting('app.current_tenant_id')::uuid.
  • Ensure least-privilege app role grants follow the existing AWCMS-Mini pattern.

Out of scope

  • Middleware collector.
  • User-agent parser.
  • Dashboard/API endpoints.
  • Rollup job implementation.
  • Online geolocation enrichment.

Acceptance criteria

  • New migration creates all three tables idempotently.
  • All tables are tenant-scoped with tenant_id.
  • RLS and FORCE RLS are enabled on all three tables.
  • Querying as the application role without withTenant does not leak tenant data.
  • Indexes support realtime online presence, 24h/7d/30d summaries, and paginated events/sessions.
  • Raw IP storage is nullable and not required for default operation.
  • login_identifier_snapshot is nullable and must not be used for anonymous public visitors.
  • ERD/data dictionary docs are updated.
  • Migration/RLS tests are added or extended.
  • bun run db:migrate passes.
  • bun test passes.
  • bun run check passes.

Security and privacy notes

  • Raw ip_address must only be populated when raw IP collection is explicitly enabled.
  • The default analytics mode should rely on ip_hash, visitor_key_hash, and parsed device/browser fields.
  • Do not store request body, cookies, authorization headers, password reset tokens, OAuth codes, or query-string secrets.
  • This schema is for analytics, not audit. High-risk actions remain in the existing audit log system.

Dependencies

Depends on #617.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions