Home›Complia›Manual
Complia · Manual

Complia Manual

Complia — User Manual

A register for data-protection complaints, and the acknowledgment clock that runs on each one. Version 4.1.3 [src: app/controllers/api.php:20].

About this manual

Every statement here was written by reading Complia's own source, and each one carries the file it came from in a bracketed src marker. Paths are relative to the folder this docs directory sits in. If a sentence disagrees with the application, the application is right and this manual has a bug — the markers exist so you can check rather than take our word for it.

Nothing is described that the code does not do. Where something is deliberately limited, that is said plainly rather than left out.

One framing point before anything else. Complia records complaints and computes dates from them. It does not decide whether you have complied with anything, and nothing in this manual is legal advice. Where the source cites a statute, this manual reports that the source cites it — not that the citation is current or correctly construed. Verify both with your own adviser.

What Complia is

A complaint arrives — through a public form or logged by a handler — and the product runs an acknowledgment clock on it, tracks it through a workflow, and keeps a register you can export [src: app/controllers/mcp.php:41].

The acknowledgment clock

The due date is the date received plus a window you configure [src: app/src/Complaints.php:44].

Why the default window is 29 days and not 30

This is the most considered decision in the product and it is worth reading, because it explains a number you might otherwise "correct".

The source cites DPA 2018 s.164A(3), which it describes as inserted by the Data (Use and Access) Act 2025 s.103, requiring acknowledgment "within the period of 30 days beginning when the complaint is received" [src: app/src/Complaints.php:5].

Its reasoning for defaulting to 29 [src: app/src/Complaints.php:11]:

The statute counts "beginning when the complaint is received" — unusual drafting, a new Act, and no case law yet. If the day of receipt is day 1, then received plus 30 days is one day late. A 29-day window is safe under both readings: the worst case is that you acknowledge a day early, which costs nothing, while the alternative worst case is a breach. If guidance settles the construction, change one setting — no code.

Two things follow for you as the operator. The window is configurable [src: app/src/Complaints.php:46], clamped between 1 and 365 days. And the choice of 29 is a deliberate margin, not a miscalculation.

Check the citation yourself. The source names a specific, recent instrument, and this manual takes no position on whether it remains current or on how it should be construed. If your adviser reads the drafting differently, the setting is where you express that.

Owners and reminders

A complaint can have a named owner, chosen from the active members and administrators; the choice is written to the complaint's log [src: app/controllers/v3.php:590]. When acknowledgment reminders are switched on in Settings, the owner and the notification address are emailed at each reminder step before the acknowledgment deadline (7, 3 and 1 days by default) and once on the day after it passes [src: app/controllers/v3.php:504]. The email gives the complaint number, the date and a link, nothing the complainant wrote. Each step is sent once and written to the complaint's log, sent or not; it is checked when someone opens the dashboard, and a daily cron call to reminders/run with the scheduled-backup token covers days nobody does [src: app/controllers/v3.php:569].

The leadership report

Leadership report shows, for the complaints received in a period (the last 12 months by default, this quarter, this year, or dates you choose): how many arrived, how many were acknowledged on time, late or not yet with the deadline passed, the median days to acknowledge and to close, the outcomes, a month-by-month table, and who holds the open complaints now [src: app/src/Complaints.php, leadershipReport()]. "On time" means acknowledged on or before the date the register computed. An empty period shows no rate rather than a zero. It downloads as CSV, is GET /api/report, and is the leadership_report MCP tool. Every figure comes from the register; the report makes no judgment about it.

The complaints procedure

Complaints procedure keeps your procedure document, every version: the file (PDF, Word, OpenDocument or plain text, checked by its content), a label, the date it takes effect, the date it is next due for review and what changed [src: app/controllers/v3.php, cp_procedure_save_file()]. The current version is the latest one already in effect. A review due within 30 days or overdue shows on the Statutory clock page. Mark a version public and the complaint form links to it at /complaints-procedure; nothing else about the procedure is public. Only an administrator uploads or changes the public link.

Related records

A complaint can carry links to related records: a subject access request in Privara, a breach record, or any other record with a reference or an address, each with a note [src: app/controllers/v3.php, complaint_link_add()]. Adding and removing one is written to the complaint's log. With your Privara address and an API key saved under Settings → Privara, Check in Privara reads the request's type, status and deadline and keeps that answer on the link with the time it was read [src: app/controllers/v3.php, cp_privara_lookup()]. Complia only reads from Privara. The key is write-only in Settings and is left out of the JSON backup. Links are carried as related in the API and the MCP complaint_detail tool.

Status

Five stored statuses — received, acknowledged, investigating, resolved, rejected [src: app/src/Complaints.php:33] — and six displayed [src: app/src/Complaints.php:36].

The sixth is ack_overdue, and it is never stored. It is derived by comparing today against the due date on a complaint that has not yet been acknowledged [src: app/src/Complaints.php:73].

The precedence is deliberate: rejected and resolved win first, so a closed complaint can never turn overdue afterward [src: app/src/Complaints.php:76]. Closed states stop the clock mattering entirely [src: app/src/Complaints.php:37].

"Acknowledged or beyond" counts investigating, resolved and rejected as well [src: app/src/Complaints.php:39] — moving straight to investigating does not leave the clock running.

The workflow is one-way

Transitions run forward only, and resolved and rejected are terminal [src: app/src/Complaints.php:85]. There is no reopen.

That is a deliberate contrast with a maintenance-ticket product, where reopening is normal. A closed complaint record is a document about what was decided; reopening it would rewrite that.

Closing requires an outcome

A complaint cannot be closed without an outcome summary, and the rule is enforced for agents as well as people [src: app/controllers/mcp.php:132].

The public form and tracking

A complainant gets a tracking token: 40 hexadecimal characters, unique, and checked for uniqueness as it is issued — the allocator throws rather than handing out a duplicate [src: app/src/Complaints.php:103].

Lookups are exact parameterized matches and a miss is a 404 [src: app/src/Complaints.php:23].

⚠️ The complainant's IP address is stored

Complia keeps the submitting IP address in the complaint row [src: app/src/Database.php:251], indexed with the timestamp [src: app/src/Database.php:258], and uses it for a per-address submission limit — ten an hour by default [src: app/src/Complaints.php:130].

That is a real trade-off and you should know which side of it you are on: the address is what makes abuse of a public form tractable, and it is also personal data about somebody making a data-protection complaint.

It is covered by the anonymization routine — ip is one of the declared columns [src: app/controllers/v3.php:809] — so a subject-access erasure clears it. But until then it is held.

Files and size limits

Attachments are PDF, PNG, JPEG, TXT or EML, up to 10 MB each, decided by the file's own bytes [src: app/controllers/v3.php:869]. When the server's PHP upload limits are lower, the pages state the lower figure [src: app/controllers/v3.php:837]. A public form that exceeds the server's whole-form limit is returned to the complainant with the reason, instead of being lost [src: app/controllers/v3.php:852].

Subject access and anonymization

Complia can export everything it holds about a complainant, and can anonymize it. The anonymization map declares which columns are cleared on each table — the complainant's name, email and IP on the complaint itself, and the original filename and uploader on each attachment [src: app/controllers/v3.php:809], [src: app/controllers/v3.php:817].

Since 4.1.1 the complaint's page carries Data request for whoever may delete complaints: download the complainant's file as JSON, or anonymize them once the complaint is closed. Anonymizing also clears their own reference and the original file name in the log line of each attachment, and deletes the attached files.

Kept: the complaint's subject, details and outcome and the handlers' notes in its log, which can name the complainant, because they are the record that the complaint was handled.

Two defects the source records having fixed

Both are worth reading, because they tell you what the current behavior is and why.

The anonymization used to miss the attachments. The attachments table declared no columns, and the shared anonymizer skips a table with none — so a run reported attachments: 0 while a file's stored name still read "Jane_Smith_SAR_letter.pdf" and the uploaded files themselves sat untouched on disk, readable through the attachment route by anyone who could view the complaint. The source calls that, in a data-protection product, "the defect the product exists to prevent" [src: app/controllers/v3.php:811].

It is fixed: the columns are declared [src: app/controllers/v3.php:817], and the files are deleted by the controller, because the shared anonymizer blanks columns and does not touch disk [src: app/controllers/v3.php:816].

The subject-access export used to be reachable by a viewer. It was gated on complaint.view, which the viewer role satisfies through its wildcard — so any read-only account could download a complainant's complete file: name, email, phone, the full complaint text and every log entry. The source records that this was proven live, a viewer session receiving HTTP 200 on the export route [src: app/controllers/v3.php:826].

It is fixed: the export now takes the same standing as anonymization [src: app/controllers/v3.php:829], on the reasoning that reading one complaint on screen is desk work, while exporting a data subject's entire record is a data-protection officer's act.

Your team's chat

Each new complaint and each acknowledgment reminder can post one line to your team's chat: paste an incoming-webhook address from Slack, Microsoft Teams, Google Chat or Discord in Settings [src: app/controllers/app.php:279]. The line carries the complaint number and the date ("New complaint #12 received. Acknowledgement due 2026-10-25."), never the complainant's name or what they wrote. The new-complaint line follows the same switch as the email; nothing is posted from the public demo. Send a test line shows what the chat answered; a refused post is written to the audit trail. The address is itself a credential, so the shareable JSON backup leaves it out.

Users and roles

Three roles [src: app/controllers/app.php:66].

Permissionviewermemberadmin
complaint.view and other .view permissionsyesyesyes
audit.viewyesyesyes
complaint.create / .advance / .note / .attach / .export—yesyes
complaint.delete — and subject-access export——yes
settings.edit, team.manage——yes

⚠️ The audit trail is still readable by a viewer

The viewer role is granted *.view — a wildcard rather than a list [src: app/controllers/app.php:69]. The audit permission is named audit.view, so the wildcard matches it and a viewer can read and export the audit trail [src: app/controllers/v3.php:16], [src: app/controllers/v3.php:26].

Note the shape of that: the subject-access export was moved off .view precisely because the wildcard made it reachable [src: app/controllers/v3.php:826], but the wildcard itself remains, and the audit trail still sits behind it. This has been raised against the product.

Backups follow the model

Complia's database backup downloads are permission-gated, requiring the settings permission [src: app/controllers/api.php:246]. A viewer cannot download the database.

The API and agent access

Five tools are exposed to an agent, of which two write [src: app/controllers/mcp.php:47]: listing complaints, complaint detail and a deadlines report on the read side; creating a complaint and advancing its status on the write side.

The instructions bind an agent to the same rules a person faces: ack_overdue is derived and never stored, resolved and rejected are terminal, and closing requires an outcome summary [src: app/controllers/mcp.php:44].

The source states the constraint directly: the agent can never invent a deadline, skip the closing-needs-an-outcome rule, or reopen a closed complaint [src: app/controllers/mcp.php:11].

The import's dry run applies the same checks the import does, row by row, and writes nothing: a row the import would skip (a complaint dated 2026-02-30 or 30/09/2026) is listed as skipped in the dry-run report, not counted ready.

What Complia does not do

It does not tell you whether you have complied. It records what arrived, computes a date from a window you set, and shows you what is overdue.

It is not legal advice, and this manual does not verify the statutory citation in the source [src: app/src/Complaints.php:5]. Confirm it, and its construction, with your adviser.

It does not reopen a closed complaint [src: app/src/Complaints.php:85].

It does not let a complaint be closed silently [src: app/controllers/mcp.php:132].

It does not keep the complainant anonymous. The submitting address is stored [src: app/src/Database.php:251], and a public complaints form is not a whistleblowing channel.

Looking after your install (from 4.0)

Updates from inside Complia

Updates in the menu opens the Updates page (/updates) [src: app/views/updates.php]. Paste your license key once and press Save key: either key works, the one in your Lemon Squeezy receipt or the one on your order page (it starts with OWX-). Check for updates asks the store for the newest release your license covers; Complia never checks by itself.

When a new version is ready, Back up and update to that version backs up your data and the current version, installs the new one and checks it. If the update is interrupted, or the new version's database step or that check fails, the previous version is put back by itself. Complia is unavailable for about a minute while this runs. The update replaces Complia's files, including any you edited, except .htaccess, .user.ini and robots.txt: an edited copy of those is kept, with the new version's copy beside it as .new to compare and merge. The Health page lists any file that differs from the version installed, so you can see that before you update. A copy of the previous version (data/updates/code-<version>/) and a backup of your data from just before the update stay in data/updates/ [src: app/src/Updater.php]. If Complia shows an error page after an update anyway, that is your way back: copy the files from code-<version>/ over the app's files, then restore the backup on the backup page. Updating from the page needs PHP's sodium and Phar extensions; if one is missing, the Updates and Health pages say so and updates stay by hand. Where Complia is hosted for you, the host updates it and the page says so.

Reaching 4.0 from a 3.x version is the last update done by hand: replace the files as before (config.php and data/ stay, and the database migrates itself on the next page load). From 4.0 on, the Updates page does it.

Starting with sample data

The installer offers Start with sample data: eight complaints across every status to try things on. You can remove it in one click. [src: app/install/index.php:133]. It is ticked by default; untick it to start empty. The sample is added when you open the first page after the installer.

When you are done with it, press the remove button in the sample banner. It asks "Remove the sample complaints? Your own records stay." Only the sample goes: your own records stay, and a sample record that one of your own records uses is kept and named in the report [src: app/src/SampleData.php].

Backups by themselves, and a copy somewhere else

On the Backups page (/restore), Back up once a day by itself is on by default; untick it to stop [src: app/views/restore.php:71]. The first visit at least 24 hours after the last backup starts the next one, after the page has loaded, so no cron job is needed. The newest 14 stay in data/backups/, with credentials left out of them (a restore keeps the live values for those). Back up now takes one straight away.

To keep a copy somewhere else as well, fill in an S3-compatible bucket (Backblaze B2, Wasabi, AWS S3 or your own MinIO): the bucket address (an https endpoint), the region, the bucket name, an optional folder, and the access key ID and secret key. Leave the address empty to keep backups on this server only. The copy goes only to the bucket you set; nothing is sent to Ownware. Once a week Complia reads the newest backup back through the restore's own dry run and keeps the result for the Health page [src: app/src/BackupTargets.php].

On Nginx: keep data/ private

On Apache, the .htaccess that ships with Complia keeps data/ (your backups included), src/, config.php and the other private files off the web. Nginx does not read .htaccess, so add these three blocks to your server block, above the block that passes .php files to PHP (Nginx uses the first one that matches):

location ~ ^/(src|data|bin|tests|controllers|deploy|cron)/ { deny all; }
location ~ ^/views/.+\.php$ { deny all; }
location ~ (^|/)(config\.php|\.demo-mode)$|\.sqlite$ { deny all; }

Then open /config.php in a browser: it should answer 403. Only the template files under views/ are closed, not the folder: the app's own saved-view addresses start with /views/ too, and they must reach it.

A complete server block for a new site (PHP-FPM 8.3; change the name, the folder and the socket to yours):

server {
    listen 80;
    server_name example.com;
    root /var/www/complia/app;
    index index.php;

    location ~ ^/(src|data|bin|tests|controllers|deploy|cron)/ { deny all; }
    location ~ ^/views/.+\.php$ { deny all; }
    location ~ (^|/)(config\.php|\.demo-mode)$|\.sqlite$ { deny all; }

    location = /install/ { rewrite ^ /install/index.php last; }
    location / { try_files $uri /index.php?$query_string; }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

The Health page

Health in the menu (/health) shows, one line each, whether the install is looking after itself: the PHP version and extensions, disk space, whether data/ is writable, email, the age of the last backup, the offsite copy, the weekly check, whether an update is waiting, and any file that differs from the version installed. Each line says what to do when it is not fine. The page only reads: it never changes anything and never contacts anything [src: app/src/Health.php].

Connect it to other tools (Zapier, Make, n8n)

Complia sends a webhook the moment something happens and accepts calls to its API, so Zapier, Make and n8n can connect to it without a plugin. The steps, the events and example recipes are in API.md, in the section "Connect it to Zapier, Make or n8n"; the API page of Complia on the store shows the same file [src: app/API.md].

New in 4.1.2: sign-in limits

New in 4.1.2 (security update): Two-step codes are now limited (10 wrong per person or per address in 10 minutes), and wrong passwords count per account as well as per address. An installed app whose database does not answer keeps its web installer closed instead of showing the install form. On Ownware Cloud, opening the app from the console starts a fresh session.

Wrong passwords are counted per address and, since 4.1.2, per account too (the email typed, in any case, stored only as a hash). After 10 wrong passwords in 10 minutes the sign-in at /login answers "Too many attempts — wait a few minutes and try again." for that address, and for that account from any address. A right password clears both counts [src: app/src/LoginRate.php:40].

Wrong two-step codes (for people who turned two-step sign-in on under /security) are counted per person and per address. After 10 wrong codes in 10 minutes the code step answers "Too many wrong codes. Wait ten minutes, then sign in again. A recovery code works in place of the code." even for the right code, and the half-finished sign-in is dropped, so the password is asked again [src: app/src/LoginRate.php:91]. The right code clears that person's count; an address's count runs out ten minutes after its wrong codes. Before 4.1.2 the code step had no limit.

If you are locked out: wait ten minutes, then sign in again with your password and a current code, or one of your recovery codes (each works once). If you lost both the authenticator and the recovery codes, whoever runs the server can turn two-step sign-in off for you in Complia's database (by default the SQLite file in data/, or your MySQL database): UPDATE users SET totp_secret = '' WHERE email = 'you@example.com'; Then sign in with your password and turn it on again under /security.

On Ownware Cloud, opening Complia from the console starts a fresh session [src: app/index.php:51].

The web installer stays closed when the database does not answer: with config.php in place, /install now answers 503 "This app is already installed (config.php exists), but its database is not answering, so the installer stays closed." and writes nothing until the database answers again. Before 4.1.2 a database outage (or a missing SQLite file) showed the install form there, and whoever found it could set up a new administrator. To install again from scratch, remove config.php first [src: app/install/index.php:20].

New in 4.1.3: security update

New in 4.1.3 (security update): no other site can put Complia's pages in a frame; the /mcp address refuses requests from other sites' pages; company sign-in is stricter; and the audit CSV treats one more first character as text. Nothing else changed: no feature, screen, setting or database table, and your data, config.php and data/ are not touched.

No other site can frame these pages. Every page now answers Content-Security-Policy: frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN, so a page on another site cannot show Complia in a hidden frame and trick a signed-in person into pressing a button [src: app/index.php:3]. The installer answers frame-ancestors 'none' and X-Frame-Options: DENY [src: app/install/index.php:4]. If you had Complia framed inside another site of your own, that frame now stays empty; link to it instead.

/mcp refuses other sites. A request whose Origin header names another site gets 403 before anything is read. A desktop or server client sends no Origin and is served as before, and so is a page at Complia's own address [src: app/src/Mcp.php:272].

Single sign-on. The sign-in now uses PKCE (S256), so an intercepted sign-in code is of no use on its own; a sign-in that comes back to a different browser session is asked to start again [src: app/src/Oidc.php:228]. With Google as the provider, a company address needs a Google Workspace account of that same domain (the token's hd claim), and Google must report the address as verified, so a personal Google account made with a work address no longer gets in [src: app/src/Oidc.php:215]. Gmail addresses work as before, other providers are unchanged, and password sign-in is not affected.

The audit CSV. A cell that starts with a carriage return now opens in a spreadsheet as text, as cells starting with = + - @ or TAB already did [src: app/src/AuditLog.php:138].

Docker. The Dockerfile now installs PHP's zip extension, so a Docker install can make the backup with files [src: app/Dockerfile:6].

Also changed, with no effect on a running install: the corner card on a public demo offers only the purchase, and API.md no longer shows the config example of a product we retired.

← Back to Complia · Quickstart · API · Test run