Auth: OTP Reset, Invite Signup, and Token Rotation

Part of the Sanchayam series. Invite-Only Signup Sanchayam is a personal finance tool. There is no public registration. New users are added by invitation from an existing user. The first user is created via a /setup endpoint that is only active when the database has no users. An invitation is a row in the invitations table: CREATE TABLE invitations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), token_hash VARCHAR NOT NULL UNIQUE, label VARCHAR, email VARCHAR, expires_at TIMESTAMP NOT NULL, used_at TIMESTAMP, created_by UUID NOT NULL REFERENCES users(id), created_at TIMESTAMP NOT NULL DEFAULT NOW() ); The token itself is never stored - only its SHA-256 hash. The invitation link sent by email contains the raw token. When the signup form is submitted, the backend hashes the submitted token and looks up the hash. If the row exists, is not yet used, and has not expired, signup proceeds and used_at is set. The raw token cannot be reconstructed from the hash, so a database leak does not expose valid invitation links. ...

April 14, 2026 · 4 min · 810 words · Sagar Nayak

Zero-Tolerance Security Model

Part of the MediaBridge series. The Design Premise Most access control systems respond to violations with a 403. You tried to access something you should not have - here is a polite rejection. Come back when you have the right permissions. MediaBridge takes a different position. Certain violation types are not mistakes. A user navigating to a URL they are not supposed to reach is an accident. A user constructing a request with a path outside their assigned root prefix is not. The system treats the latter as an active intrusion attempt and terminates the session immediately, rather than returning a 403 and letting the session continue. ...

February 13, 2026 · 6 min · 1088 words · Sagar Nayak

Collecting IMAP Credentials Without Storing Plaintext

Part of the PurelyManage series. The Problem Migrating an organization’s email means moving every mailbox from the old provider into PurelyMail. Each mailbox requires the source IMAP credentials: the email address and password the user logs in with on the old system. The naive way to collect these is to email everyone a spreadsheet and ask them to fill in their passwords. That spreadsheet then sits in someone’s inbox or a shared drive, plaintext, accessible to anyone who can read it. It is also error-prone: users mistype passwords, the admin has no way to know if a credential is correct until the migration job fails. ...

January 8, 2026 · 5 min · 1017 words · Sagar Nayak

Rogue Admin Protection with a 24-Hour Deletion Queue

Part of the PurelyManage series. The Problem Any admin panel that manages real data has a deletion problem. The moment you give someone a delete button, you are one misclick away from losing something that took time to set up. In a single-user tool this is manageable: you know who deleted it because it was you. In a multi-admin panel it is more complicated. PurelyManage can have multiple sysadmin accounts. Any of them can delete email users, domains, and routing rules. The operations go directly to PurelyMail via their API, meaning the moment the request is made, the resource is gone. There is no recycle bin, no undo, no recovery path. ...

December 9, 2025 · 6 min · 1078 words · Sagar Nayak

Storing Credentials Securely: AES-256-GCM and JWT

Part of the PurelyManage series. PurelyManage handles two categories of sensitive data: IMAP passwords submitted by users during migration, and the session tokens that keep sysadmins logged in. Neither can be stored carelessly. This post covers how both are handled and why each design decision was made the way it was. Encrypting IMAP Credentials at Rest The problem with storing passwords When a user submits IMAP credentials for a migration job, the backend needs to hold onto those credentials until the job actually runs, which might be minutes or hours later depending on the queue. They have to live in the database. The question is in what form. ...

December 3, 2025 · 7 min · 1313 words · Sagar Nayak