Salvatore Vivolo
Back to Blog
9 min read

From industrial safety to software security: two weeks reviewing IkigaiApp with AI

How a hosting incident prompted a security and privacy review: access control, encryption, two-factor authentication, and a way of working with AI while keeping human responsibility.

CybersecurityAIPrivacy

I have spent about twenty years in industrial automation. In that world a machine is designed to be safe: you analyse the risks first, then design guards, interlocks and procedures, and you prove they work. You don't add safety afterwards.

Industrial safety and cybersecurity address different risks, but the method carries over: make the risks explicit, design the controls, verify them. In October 2026 I applied it to IkigaiApp, a personal project. This is the story of two weeks spent reviewing the app's security and privacy: controls to fix, protections to verify, decisions to document. AI was the tool; the decisions stayed with me. Not every control described here was introduced in those two weeks: the review also covered protections already in place.

The app and the trigger

IkigaiApp is a self-assessment web app built around the Japanese idea of ikigai: a survey, a personal profile, a narrative of the result written with AI, and some personal tools such as a journal and flashcards. About 170 people have signed up. It runs on managed services: an edge platform for the website, a container platform in the EU for the API, a managed database in the EU.

On 21 October 2026 Railway, the provider I use for the API, disclosed an incident: an unauthorised party may have accessed customers' encrypted environment variables and, for some of them, obtained the decrypted contents. That is where the secrets live: database passwords, the keys that sign sessions, the keys that protect the encrypted data. The initial disclosure had not yet identified all affected customers.

I assumed the worst. Every secret was regenerated, including the master key that protects the database encryption keys. I checked the available logs of every service from the start of the exposure window and found no sign of misuse. I enabled two-factor authentication on every provider account and documented the incident in the breach register, with the risk assessment and the reasons why, on the evidence available, notifying the authority was not required.

That could have been the end. Instead it became the start of a full review.

Step 1: who can see what

The most valuable exercise was also the least glamorous: reviewing every API route, all 283 of them, with three questions. Who may call it? Whose data can it touch? What does it return?

It found 14 problems and 7 points to verify. Some were subtle: a check meant to answer "is this user an administrator?" was always true, because a function was referenced instead of called. Others were classic: changing an ID in a request could show someone else's records. All of them were fixed.

Every fix shipped with a test that fails on the old code and passes on the new one. This matters more than it seems: a test that goes from red to green is evidence about that case, and it stops the defect from coming back. It does not say there are no other problems, which is why the review covered every route, not just the suspicious ones.

The same review removed quieter leaks:

  • responses about other users now carry only an ID and a display name;
  • the app no longer reveals whether an e-mail address has an account;
  • rate limits slow down anyone trying to automate guesses.

Step 2: protecting data in the database

Personal data (e-mail, name, nickname) and what users write in their personal tools (journal, to-dos, Daily Anchors, flashcards) are encrypted by the application before reaching the database, using AES-256-GCM. The keys follow an envelope scheme: data keys are encrypted by a master key that never lives in the database. Every key has a version, so it can be replaced without downtime. The master key has already been rotated; rotating the data keys is one of the next steps.

Encrypted fields are hard to search, so next to some of them the app stores a blind index: a keyed hash of the normalised value. It lets the app find a user by e-mail or enforce unique nicknames without decrypting anything and without storing the value in plain text. It does not make the data anonymous: equal values produce the same index. That is why the index needs protecting together with its keys.

Session cookies are HttpOnly, Secure and SameSite: page scripts cannot read them, they only travel over encrypted connections, and the browser does not attach them to requests started by other sites. Links that act without a login are signed and expire. In production the server refuses to start if a required secret is missing: a forgotten setting cannot silently fall back to a weak default.

Step 3: a second factor for the keys to the kingdom

The administrator account has extensive privileges, so it now requires two-factor authentication. I chose TOTP, the open standard (RFC 6238) behind Google Authenticator, Authy, Microsoft Authenticator and Apple Passwords.

It is implemented with the platform's standard cryptography and verified against the official test vectors. Codes cannot be reused. Recovery codes are stored only as slow hashes. A break-glass procedure for a lost phone exists, can only be run by me, and is logged.

Step 4: privacy as a design constraint

Some privacy decisions cost something, and that is the point.

  • I gave up analytics linked to accounts. Usage statistics no longer store the user ID or the IP address: I can see which pages are used, not who uses them. A single first-party cookie remains: it holds a random value, groups the pages of one visit and lasts 30 minutes. The statistics stay in our own database, are used only in aggregate, and are neither combined with other data nor shared. I assessed this set-up against the Italian data protection authority's guidelines: under these conditions the cookie can be treated as a technical cookie, which is why the app shows no consent banner.
  • Data expires on its own. Statistics after 13 months, technical data after 90 days, AI usage logs after 12 months, security logs within a year. The database enforces the expiry. Encrypted backups follow their own schedule: they are replaced within 6 months.
  • Deleting an account deletes the data linked to it. A test fills every application collection with a user's data, deletes the account and checks that nothing is left. Records left behind by accounts deleted in the past were cleaned up too. What remains is security logs until they expire and backups until they are replaced, as the privacy policy states.
  • Less data in logs. Secrets and e-mail addresses are redacted. Where a reference is needed I use IDs and keyed hashes: these are pseudonymous, not anonymous, which is why they expire.
  • No fake switches. Consent toggles that did nothing were removed. I don't use app data for marketing, don't sell it and don't train models on it. What each provider receives, the AI provider included, is set out in the privacy policy; the AI provider does not use this data to train its models.
  • The privacy policy starts from how the app actually works. I rewrote the public legal pages by comparing them with the code and the services used, not by starting from a template. When the code changes, the policy changes with it.

Step 5: AI inside the product

The app uses AI to write the narrative profile, generate flashcards and translate administrator messages. Each feature receives only what it needs. The profile, for example, gets derived categories and never a name, e-mail or free-text answer. Generated content is labelled "Generated with AI", so users can see where AI is involved. The matching between similar profiles is deterministic and explainable, not machine learning.

The method: AI for speed, human review for judgement

I didn't write most of this code by hand. I worked with two AI assistants: one analysed the code, wrote specifications and reviewed every change; the other implemented them on separate branches. I made every decision, merged every change and ran every production operation myself.

Three practices made the difference:

  1. Rules in the repository. A file tells coding agents what they must never do: put secrets in code or logs, weaken a security control, change behaviour without tests.
  2. Evidence, not reassurance. Every batch ended with a report comparing the full test suite with the main branch test by test, not just the totals.
  3. Two-step production changes. Every data change is a script that first does a dry run and prints only counts. The real run comes after reviewing the numbers, and after a fresh encrypted backup.

Reviewing the changes caught what tests missed. One change passed all its own tests but would have looked for data to delete in three collections with the wrong names, leaving it in the database. The tests used the same wrong names. It was caught by reading the diff, before production, and the fix made those names come from the data models. Another time, simply checking that a backup could be decrypted revealed that the backup script was mixing a newline into the encryption key. Both were fixed before they could do harm. The lesson: a second AI assistant helps, but it is not independent verification. The final read has to stay human.

Where it stands, honestly

Today I know what is protected, how I verify it and what is missing. Every fix has a test, every decision has a document. I have not had an external assessment, and this review does not replace one.

It is not finished. Next on the list:

  • automatic dependency updates with continuous integration;
  • a fully green test suite that blocks every merge;
  • rotation of the data encryption keys;
  • automatic encrypted backups;
  • automatic alerts on anomalies;
  • passkeys, and one day an external penetration test.

What I would tell anyone starting a project

  1. Audit access control route by route before adding features.
  2. Encrypt sensitive fields in the application and plan key rotation from day one.
  3. Collect less: data you don't keep needs no protection and no justification. For what remains, decide its purpose and its expiry.
  4. Make the privacy policy describe the code, then keep them in sync.
  5. Use AI for speed and people for judgement. Ask for evidence, not reassurance.
  6. For every production data change: backup, dry run, review, run, verify.

Safety by design works for software too. The tools are new; the discipline is not.

The full technical case study, updated at every step, is here: IkigaiApp security and privacy case study.

If you have been through a similar review, or would like to discuss this approach, get in touch.

Share this article