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 cacheImportant 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