Skip to content

ADR-0018: ISMS Information System

Accepted Cross-Project Territory-Specific

Date: 2026-03-24 | Last Revised: 2026-04-16 | Approved by allies: 2026-04-16

Compliance: ISO 27001

Context

The script-development organization operates under ISO 27001 certification. Compliance governance — policies, controls, procedures, review schedules, and evidence — needs a centralized system. The industry default is wiki-based tools like Confluence, but these function as document graveyards: information enters but is neither programmatically queryable nor actionable. No one knows when a control review is due without manually checking.

The team consists of 10 people:

  • 4 ISMS workers who actively maintain policies, controls, reviews, and evidence. One of the four is not a programmer.
  • 6 colleagues who need read access to policies only.

Requirements:

  1. Policies must be version-controlled with full change history (A.5.33 — protection of records)
  2. Control review schedules must integrate with Google Calendar
  3. Evidence (images, PDFs) must be stored and linked to controls — evidence is sensitive and must be auth-gated
  4. The non-programmer must be able to interact with the system
  5. Policy-only colleagues must have a clean, browsable interface limited to policies
  6. Notifications for policy updates, discussions, and overdue reviews
  7. Discussion/comments on controls and policies

Architecture Pivot

An initial design based on VitePress + Cloudflare Workers + local MCP server was evaluated and rejected during deliberation. As notification routing, discussion threads, user preferences, and evidence auth were added, the architecture became a patchwork of loosely connected components — straying from the team's established workflow. A Laravel + Vue application consolidates all of these into a single, familiar stack.

Decision

A Laravel 12 + Vue 3 application with reference documents as markdown files in the repo and operational data in PostgreSQL. Deployed on Fly.io. Evidence stored in a private R2 bucket, accessed through the Laravel backend.

The Hybrid: Files + Database

The core architectural insight is that ISMS data splits cleanly into two categories: reference documents (rarely change, need git history, must be agent-searchable) and operational data (changes frequently, needs relationships, needs queries).

In Files (repo)In Database (PostgreSQL)
Policies (markdown)Control operational state (owner, status, next review)
Procedures (markdown)Reviews + review history
Control descriptions + implementation (markdown)Evidence records (metadata + R2 keys)
User accounts + roles + notification preferences
Discussion comments
Google Calendar sync state
Notification log

Why reference documents as files:

  • Policies, procedures, and control descriptions are versioned documents — git provides immutable change history, satisfying A.5.33 (protection of records)
  • These documents need to be searchable by agents — grep, read, and diff work natively on markdown files
  • They change rarely — a database row is the wrong abstraction for a document that updates quarterly
  • The Laravel app reads files from disk and renders them in the Vue frontend — the file is the source of truth, the app is the presentation layer

Why operational data in the database:

  • Reviews, evidence, comments, and preferences change frequently, need relationships, and benefit from queries
  • Calendar sync, notification routing, and user preferences require state management that files handle poorly
  • Discussion threads are relational data — not documents

The control split: A control lives in two places. The markdown file holds the what and how (description, implementation details, applicable territories). The database holds the who and when (owner, status, review dates). The app merges both when rendering a control page: markdown for the body, database for the operational sidebar.

Application Structure

The repo lives at backtocode/isms and is added as a submodule to the war room at territories/isms.

isms/
  app/                                  # Laravel application
    Actions/                            # final readonly action classes
    DTOs/
    Models/
    Services/
      MarkdownService.php              # Reads + parses markdown files with frontmatter
    Http/
      Controllers/
      Requests/
    Notifications/                      # Policy update, review due, discussion reply
    Console/
      Commands/
        SyncCalendarCommand.php         # Scheduled: sync reviews → Google Calendar
        NotifyOverdueReviewsCommand.php # Scheduled: flag overdue reviews
        ValidateFileDatabaseSyncCommand.php  # CI: verify file ↔ DB consistency
  resources/
    js/                                 # Vue 3 frontend
      Pages/
        Policies/                       # Renders markdown policies
        Controls/                       # Merged view: markdown body + DB sidebar
        Dashboard/                      # Overview, upcoming reviews, recent activity
    policies/                           # Markdown policy files (source of truth)
      information-security-policy.md
      access-control-policy.md
      acceptable-use-policy.md
      ...
    controls/                           # Markdown control files (description + implementation)
      A.5.12.md                         # One file per control
      A.5.13.md
      A.5.33.md
      A.8.15.md
      ...
    procedures/                         # Markdown procedure files
      incident-response.md
      risk-assessment.md
      ...
  database/
    migrations/
  tests/

Control Markdown Format

Each control has a markdown file in resources/controls/. The file holds the reference content (description, implementation), while the database holds operational state.

yaml
# resources/controls/A.8.15.md
---
id: A.8.15
title: Logging
category: A.8 Technology Controls
---

## Description

User activities, exceptions, faults and information security events
shall be recorded in audit logs, retained, protected and analysed.

## Implementation

Per-entity audit tables with SHA-256 hash chains for tamper detection.
Explicit action-level logging — no model observers. Point-in-time user
snapshots capture actor identity at the moment of the action.

See ADR-0001 for full design. Deployed for Tier 1 entities (Issue,
User, AppSetting, GithubConnection, TimeLog) via PRs #321, #357-360,
#373. Tier 2 (10 remaining entities) scoped but not started.

## Applicable Territories

- kendo

The id in frontmatter links to the control_id column in the database. The app's MarkdownService reads and parses these files. The Vue frontend renders a merged view: markdown body for the content area, database fields for the operational sidebar (owner, status, next review, evidence list, comments).

Database Schema (Key Tables)

sql
-- Controls: operational state only — description and implementation live in markdown files
controls
  id                BIGINT UNSIGNED PK
  control_id        VARCHAR UNIQUE      -- e.g. "A.8.15" — links to resources/controls/A.8.15.md
  status            VARCHAR             -- gap | in-progress | implemented | not-applicable
  owner_id          BIGINT UNSIGNED FK  -- users table
  review_cycle_days INT                 -- e.g. 180 for 6 months
  next_review_at    DATE
  created_at        TIMESTAMP
  updated_at        TIMESTAMP

-- Reviews: dated review records per control
reviews
  id                BIGINT UNSIGNED PK
  control_id        BIGINT UNSIGNED FK
  reviewer_id       BIGINT UNSIGNED FK  -- users table
  outcome           VARCHAR             -- satisfactory | action-required | escalated
  findings          TEXT
  actions           TEXT NULLABLE
  reviewed_at       DATE
  next_review_at    DATE                -- Updates control's next_review_at
  created_at        TIMESTAMP
  updated_at        TIMESTAMP

-- Evidence: metadata for files stored in R2
evidence
  id                BIGINT UNSIGNED PK
  control_id        BIGINT UNSIGNED FK
  uploaded_by_id    BIGINT UNSIGNED FK
  label             VARCHAR
  r2_key            VARCHAR             -- e.g. "A.8.15/2026-03-15-log-sample.png"
  mime_type         VARCHAR
  file_size         INT UNSIGNED
  created_at        TIMESTAMP

-- Comments: threaded discussions on controls and policies
comments
  id                BIGINT UNSIGNED PK
  commentable_type  VARCHAR             -- "control" | "policy"
  commentable_id    VARCHAR             -- control FK or policy slug
  user_id           BIGINT UNSIGNED FK
  parent_id         BIGINT UNSIGNED FK NULLABLE  -- for threading
  body              TEXT
  created_at        TIMESTAMP
  updated_at        TIMESTAMP

-- Users
users
  id                BIGINT UNSIGNED PK
  name              VARCHAR
  email             VARCHAR UNIQUE
  role              VARCHAR             -- isms-worker | colleague
  notification_preferences JSON         -- per-event channel preferences
  created_at        TIMESTAMP
  updated_at        TIMESTAMP

Access Model

RoleCan SeeCan Do
ISMS worker (4 people)EverythingFull CRUD on controls, reviews, evidence, comments. Manage policies (via git).
Colleague (6 people)Policies + procedures onlyRead policies, comment on policies

Laravel middleware enforces role-based access. No Cloudflare Access layer needed — the app handles auth directly.

Evidence Storage

Evidence is sensitive and auth-gated. No public URLs.

Evidence files are stored in a private Cloudflare R2 bucket. The Laravel backend handles both uploads and downloads — R2 credentials live in the server environment (Fly.io secrets), never on local machines.

isms-evidence/                       # Private R2 bucket
  {control-id}/
    {date}-{description}.{ext}

Upload flow: Vue form → Laravel controller → validates + stores in R2 → creates evidence record. Download flow: Vue requests evidence → Laravel controller checks auth → streams from R2.

The S3-compatible SDK (league/flysystem-aws-s3-v3) connects Laravel to R2.

Google Calendar Sync

A Laravel scheduled command (SyncCalendarCommand) that:

  1. Reads all controls with next_review_at set
  2. Compares against existing events in a shared Google Calendar (matched by control-ID-based event identifier)
  3. Creates, updates, or removes calendar events
  4. Each event includes: control ID, title, link to the control page in the app
  5. The control owner receives a calendar invite

Runs daily via Laravel's scheduler on Fly.io. Uses the Google Calendar API via a service account.

Notifications

Laravel notifications with per-user channel preferences stored in users.notification_preferences:

json
{
  "policy_updated": ["email"],
  "review_due": ["email", "google_chat"],
  "review_completed": ["google_chat"],
  "review_overdue": ["email", "google_chat"],
  "discussion_reply": ["google_chat"]
}
EventDefault AudienceAvailable Channels
Policy published/updatedAll 10 colleaguesEmail, Google Chat
Control review due (7 days before)Control ownerEmail, Google Chat
Control review overdueISMS team (4) — escalationEmail + Google Chat (forced both)
Review completedISMS team (4)Email, Google Chat
Discussion reply / mentionMentioned user(s)Email, Google Chat

Google Chat notifications via incoming webhook. Email via Laravel's mail system.

Markdown Rendering

A MarkdownService reads and parses markdown files with YAML frontmatter. Used for both policies and control descriptions.

Policies (resources/policies/) carry minimal frontmatter:

yaml
---
title: Information Security Policy
version: "1.2"
effective_date: 2026-01-15
owner: jane@example.com
review_cycle: 12 months
---

Controls (resources/controls/) link to the database via id:

yaml
---
id: A.8.15
title: Logging
category: A.8 Technology Controls
---

The Vue frontend renders policies as standalone pages. Controls render as a merged view: markdown body (description + implementation) in the content area, database fields (owner, status, next review, evidence, comments) in the operational sidebar.

Version history comes from git — the app can shell out to git log for a specific file or use a pre-built changelog generated at deploy time.

File ↔ Database Sync Validation

Since controls live in two places (markdown file + database row), consistency must be enforced. An artisan command (ValidateFileDatabaseSyncCommand) checks:

  • Every control_id in the database has a matching markdown file
  • Every control markdown file has a matching database record
  • The id in frontmatter matches the filename convention

Runs as a CI check and can be invoked manually. Failures block deployment.

Statement of Applicability

The SoA is generated dynamically by merging the controls table (operational state) with control markdown files (description). A dedicated page in the Vue app lists all controls with their status, owner, last review date, next review date, and description summary. Filterable and exportable for auditors.

Options Considered

OptionVerdictReason
Confluence / wiki-basedRejectedDocument graveyard. Not programmatically queryable. No agent access. No version control for compliance.
Pure markdown + VitePress (no server)RejectedLoses structured queries. Can't handle discussions, notifications, user preferences, or evidence auth.
Markdown + VitePress + Cloudflare Workers + MCP serverRejectedPatchwork architecture — multiple loosely coupled components for problems a single app solves natively. Strays from team's established Laravel + Vue workflow.
Git LFS for evidenceRejectedAdds friction for non-programmer. Team already uses R2/S3 extensively.
All data in database (including policies and control descriptions)RejectedReference documents are versioned, rarely change, and need agent searchability — git provides immutable history (A.5.33) better than a database.
Laravel + Vue app with reference documents as markdown filesAcceptedSingle familiar stack. Auth, notifications, evidence, discussions, calendar — all handled by the framework. Policies, procedures, and control descriptions stay as files for version control and agent access. Operational data (reviews, evidence, preferences) lives in the database where it belongs.

Consequences

Positive

  • Single stack — Laravel + Vue is the team's established workflow (kendo, brick-inventory). No new tooling to learn.
  • Reference documents in git — policies, procedures, and control descriptions have immutable change history, satisfying A.5.33. Agents can grep and read files natively.
  • Auth built-in — Laravel middleware handles role-based access. No external auth layer needed.
  • Notifications built-in — Laravel notifications with per-user channel preferences. No external notification routing.
  • Discussions built-in — threaded comments in the database. No external discussion system.
  • Evidence auth built-in — Laravel streams from R2 after checking auth. No separate Worker needed.
  • Calendar sync — a single scheduled command. No external sync infrastructure.
  • Non-programmer has a UI — no git or CLI required for most operations. Claude Code is a bonus interface, not the primary one.

Negative

  • More infrastructure than a static site (server, database, deployment pipeline)
  • Reference document changes (policies, control descriptions) require git commits — colleagues can't edit them in the UI (by design — these are controlled documents)
  • Control data lives in two places (file + database) — sync validation required
  • Application maintenance burden (security updates, framework upgrades)

Risks

  • File / database drift — If a markdown file is renamed or deleted without updating the database (or vice versa), the app shows broken controls. Mitigation: ValidateFileDatabaseSyncCommand runs in CI and blocks deployment on mismatch.
  • Calendar sync drift — If the scheduled command fails, calendar events become stale. Mitigation: daily schedule + monitoring via Fly.io health checks.
  • R2 orphaned files — Evidence deleted from database but not from bucket. Mitigation: the DeleteEvidenceAction deletes from both R2 and database in a transaction. Periodic reconciliation as safety net.

Enforcement

WhatMechanismScope
Role-based accessLaravel middleware + architecture testAll routes
File ↔ DB syncValidateFileDatabaseSyncCommand in CI — blocks deploy on mismatchresources/policies/*.md, resources/controls/*.md
Evidence cleanupDeleteEvidenceAction handles R2 + DB atomicallyEvidence CRUD
Calendar sync healthFly.io scheduled command monitoringSyncCalendarCommand
Action pattern compliancePest architecture testsapp/Actions/

Implementation

TerritoryStateNotes
isms (backtocode)Not StartedRepo to be created. Laravel 12 + Vue 3 + PostgreSQL. Deploy on Fly.io. First policy: information security policy. First control: A.8.15 (evidence from ADR-0001).

Architecture documentation for contributors and collaborators.