ProjectsOpen-source product

GetSecret

An open-source platform for sharing a secret through a link that expires or can be read only a limited number of times.

Type
Secret-sharing platform
Status
Open source
Context
Independent

Overview

GetSecret shares sensitive information through a link that can expire, or that can be read only a limited number of times. The repository is a monorepo: a NestJS API, a web app, a TypeScript SDK, a CLI, and a Chrome extension. I created it and I maintain it.

Problem

Credentials sent in Slack, email, or a notes app remain available long after they were needed. Most teams already have a vault for secrets that live for months. They still paste one-off passwords into chat because that is the fastest option in the moment.

Why I built it

I wanted a share that is temporary by default: create a secret, send a link, and have it go away after it is read or after it expires. It is open source because people should be able to inspect a tool before they paste a secret into it, and they should be able to run it themselves.

My role

I design and maintain the platform: the API, the web app, the SDK, the CLI, and the extension. This is an independent project, not work I share with a product team.

Technical architecture

Clients never talk to the database. The web app, SDK, CLI, and extension call one API. The API returns a slug, an expiry, and a view limit. Each client builds the share URL itself.

Web app, SDK, CLI, extension
              |
         NestJS API
       /      |      \
PostgreSQL   Redis   Encryption
(Prisma)   rate limits  AES-256-GCM
           and cache

Important engineering decisions

  • The public link is an opaque slug, not a hash of the row id. Revoking or regenerating a link removes access and leaves the stored secret alone.
  • PostgreSQL is the source of truth. Prisma models the secret, its view count, expiry, and optional attachment.
  • Redis is used for rate limits, including wrong-password attempts, and for a short-lived cache. A read still checks the database before it counts a view.
  • Docker Compose runs Postgres, Redis, the API, and an nginx gateway, which is the self-hosting path.

Security considerations

Secrets without a passphrase are encrypted on the server with AES-256-GCM before they are stored. The key comes from ENCRYPTION_KEY and is required to start the API. The server can decrypt those secrets. That is server-side encryption, not a zero-knowledge guarantee.

Secrets created with a passphrase take a different path. The client encrypts the payload with AES-GCM, using a key derived with PBKDF2, and the API stores that ciphertext. The database also stores a hash of the passphrase so a read can be rejected before the ciphertext is returned.

A read is allowed only while the secret is not deleted, not expired, and still under its view limit.

Challenges and trade-offs

One-time retrieval is a concurrency problem. Two people can open the same link together. Counting the view in application code, after a SELECT, lets both of them succeed.

  • Server-side encryption is easier to use, and it means the operator of the server can decrypt secrets that were not created with a passphrase.
  • Client-side encryption avoids storing that plaintext, and it asks the recipient for the passphrase. The product supports both instead of pretending one model fits every share.
  • Caching the secret record helps repeat lookups. The cache is not allowed to serve a secret that the database would reject, so an expired or deleted row is purged before a read continues.

The read counts only when this update returns a row. The conditions and the increment happen in one statement.

UPDATE "SecureSecret"
SET "viewCount" = "viewCount" + 1,
    "isDeleted" = CASE
      WHEN "maxViews" IS NOT NULL
       AND "viewCount" + 1 >= "maxViews" THEN true
      ELSE "isDeleted"
    END
WHERE "slug" = $slug
  AND "isDeleted" = false
  AND ("expiresAt" IS NULL OR "expiresAt" > now())
  AND ("maxViews" IS NULL OR "viewCount" < "maxViews")
RETURNING *;

Outcome

  • The application is live, open source, and self-hostable with Docker.
  • A TypeScript SDK is published for integrations that should not reimplement the API.

What I learned

The difficult part of "read once" was making the view count atomic, not designing the form.

Separating the link from the secret made expiry and revocation ordinary database operations.

A tool in this category is judged by the trust model you can explain. I would rather describe the two encryption paths than claim a guarantee the server-side path does not provide.

Links

React · TypeScript · NestJS · PostgreSQL · Prisma · Redis · Docker