Self-hosted secrets management for development teams.
Stop committing secrets to Git. Stop sending .env files over Slack.
envShare encrypts every variable at rest and lets each developer pull exactly what they need.
envShare is a self-hosted alternative to services like Doppler or 1Password Secrets. You run the server on your own infrastructure — your keys never leave your control.
Each secret variable can be either:
| Type | Description | Example |
|---|---|---|
| Shared | One value for the whole team. Everyone pulls the same thing. | DATABASE_URL, REDIS_URL, STRIPE_PUBLIC_KEY |
| Personal | Each developer has their own encrypted copy. | AWS_ACCESS_KEY_ID, STRIPE_SECRET_KEY, local DB passwords |
Every value is encrypted with AES-256-GCM. The master encryption key never touches the database — lose it and the data is unrecoverable, so back it up.
1. Generate secrets
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" # MASTER_ENCRYPTION_KEY
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" # JWT_SECRET2. Create .env in the project root
POSTGRES_PASSWORD=your_db_password
JWT_SECRET=<64-char hex from above>
MASTER_ENCRYPTION_KEY=<64-char hex from above>
ALLOWED_ORIGINS=*Warning: Back up
MASTER_ENCRYPTION_KEYsecurely. Losing it means losing access to every stored secret permanently.
3. Start the server
docker compose up -d
docker compose exec backend npx prisma migrate deployThe API is now available on port 3001.
4. HTTPS with automatic certificates (Caddy)
ENVSHARE_DOMAIN=secrets.yourdomain.com docker compose -f docker-compose.https.yml up -dThe CLI is a standalone binary — no Node.js required on developer machines.
macOS / Linux (Homebrew)
brew install s-pl/envshare/envshareWindows (Scoop)
scoop bucket add envshare https://github.com/s-pl/scoop-envshare
scoop install envshareLinux (manual)
sudo curl -fsSL https://github.com/s-pl/envShare/releases/latest/download/envshare-linux-x64 \
-o /usr/local/bin/envshare && sudo chmod +x /usr/local/bin/envshareOnce installed, keep it up to date with:
envshare update# Point the CLI at your server
envshare url http://your-server:3001
# Create your account
envshare register
envshare login
# Create a project and link your repository
envshare project create
cd my-app
envshare init
# Push your .env — an interactive selector lets you choose which variables to upload
envshare push
# Invite teammates
envshare project invite alice@company.com --role DEVELOPER
envshare project invite bob@company.com --role VIEWER# Point at the same server
envshare url http://your-server:3001
# Create your account
envshare register
envshare login
# Link your local folder and pull secrets
cd my-app
envshare init
envshare pullIf any personal secrets are not yet set, the pulled .env will contain a placeholder:
DATABASE_URL= # pending — run: envshare set DATABASE_URL "your-value"Set your value with:
envshare set DATABASE_URL "postgres://localhost/myapp_dev"Every project member has one of three roles. Roles are per-project — the same user can be Admin on one project and Viewer on another.
| Permission | Viewer | Developer | Admin |
|---|---|---|---|
| View and pull secrets | yes | yes | yes |
| Push secrets | — | yes | yes |
| Set / update secret values | — | yes | yes |
| View secret history | yes | yes | yes |
| Create environments | — | yes | yes |
| Invite members | — | — | yes |
| Change member roles | — | — | yes |
| Delete secrets | — | — | yes |
| Delete project | — | — | yes |
| View project audit log | — | — | yes |
Full documentation: wiki/User-Guide.md
flowchart LR
subgraph workstation["Developer workstation"]
CLI["envshare CLI\n(standalone binary)"]
end
subgraph server["Server · Docker Compose"]
Caddy["Caddy\nTLS + reverse proxy\n(optional)"]
API["Backend API\nExpress · Prisma\n:3000"]
DB[("PostgreSQL 16\n:5432")]
end
GH["GitHub Releases\n(self-update)"]
CLI -->|"HTTPS /api/v1/*"| Caddy
Caddy -->|"/api/*"| API
API --> DB
CLI -.->|"envshare update"| GH
Every secret goes through two layers of encryption. The master key (an environment variable on the server) wraps a per-project key, which encrypts each individual value. This means rotating the master key requires re-wrapping project keys, but secrets themselves do not need to be re-encrypted.
graph TD
M["MASTER_ENCRYPTION_KEY\nenv var — never stored in DB"]
P["Project Key\nrandom 32 bytes per project\nstored encrypted in DB"]
SK["Secret key name\nAES-256-GCM + random IV"]
SV["Shared value\nAES-256-GCM + random IV"]
PV["Personal value\nAES-256-GCM + random IV"]
KH["Key hash\nHMAC-SHA256 — deduplication only"]
M -->|"wrapKey / unwrapKey"| P
P --> SK
P --> SV
P --> PV
P --> KH
Access tokens are short-lived (15 min) and stored in memory. Refresh tokens are single-use and rotated on every request, so a stolen token becomes invalid as soon as the real client uses it.
sequenceDiagram
actor Dev as Developer
participant CLI
participant API
participant DB
Dev->>CLI: envshare login
CLI->>API: POST /auth/login
API->>DB: verify password (bcrypt)
API->>DB: INSERT refresh_token
API-->>CLI: accessToken + refreshToken
CLI->>CLI: store tokens in config
Note over CLI,API: 15 minutes later — access token expires
CLI->>API: request with expired token
API-->>CLI: 401 Unauthorized
CLI->>API: POST /auth/refresh
API->>DB: DELETE old token (single-use rotation)
API->>DB: INSERT new refresh_token
API-->>CLI: new accessToken + refreshToken
CLI->>API: retry original request
sequenceDiagram
actor Dev as Developer
participant CLI
participant API
participant DB
Note over Dev,DB: Push
Dev->>CLI: envshare push
CLI->>CLI: parse .env, classify keys (shared / personal)
CLI->>API: POST /sync/:projectId/push
API->>API: unwrap project key with MASTER_ENCRYPTION_KEY
loop each secret
API->>API: AES-256-GCM encrypt(value, projectKey)
API->>DB: upsert secret + create version record
end
API-->>CLI: { created, updated, sharedUpdated }
CLI-->>Dev: progress bar + summary
Note over Dev,DB: Pull
Dev->>CLI: envshare pull
CLI->>API: GET /sync/:projectId/pull
API->>DB: SELECT secrets + this user's personal values
API->>API: decrypt all values with project key
API-->>CLI: [{ key, value, filePath, environmentName }]
CLI->>CLI: group by filePath, write each .env (mode 0600)
CLI-->>Dev: list of files written
| Control | Detail |
|---|---|
| Encryption at rest | AES-256-GCM with a random IV per secret. Authentication tag prevents tampering. |
| Master key | Never stored in the database. Server refuses to start without it. |
| Passwords | bcrypt with 12 rounds. |
| Access tokens | 15-minute expiry, kept in memory only — never written to disk. |
| Refresh tokens | Single-use. Rotated on every refresh. Stored as a hash in the database. |
| Rate limiting | 20 requests / 15 min on auth endpoints. Global 500 req / 15 min limit. |
| Account lockout | Locked after 10 consecutive failed login attempts. |
| Audit log | Every push, pull, and member change is recorded with actor, IP, and timestamp (ISO 27001 A.12.4.1). |
| GDPR | Audit logs auto-purged after 365 days (configurable). Consent recorded at registration. |
Full threat model: SECURITY.md
| Command | Description |
|---|---|
envshare url <url> |
Set the backend API URL |
envshare register |
Create a new account |
envshare login |
Authenticate and store tokens |
envshare init |
Link the current directory to a project |
envshare version |
Show version, server URL, and auth status |
envshare update |
Download and install the latest release |
| Command | Description |
|---|---|
envshare push |
Upload .env — interactive variable selector |
envshare push --all |
Push every variable without prompts (CI-friendly) |
envshare push --env staging |
Tag secrets with an environment name |
envshare push --dry-run |
Preview what would be pushed without uploading |
envshare pull |
Download secrets and write .env files |
envshare pull --env staging |
Pull only a specific environment |
envshare pull --output .env |
Write everything to a single file |
envshare set KEY "value" |
Set your personal value for a key |
envshare set KEY "value" --shared |
Update the shared value (visible to all) |
| Command | Description |
|---|---|
envshare list |
List all secret key names in the project |
envshare history KEY |
Show the full version history of a secret |
envshare audit |
Show the project audit log (Admin only) |
envshare delete KEY |
Delete a secret from the project |
| Command | Description |
|---|---|
envshare project create |
Create a new project |
envshare project invite <email> --role <role> |
Invite a team member |
envshare project members |
List current members and their roles |
envshare project set-role <email> <role> |
Change a member's role |
envshare project remove <email> |
Remove a member from the project |
envshare ui # full-screen terminal UI — browse secrets, push, manage teamAdd # @shared to any line in your .env:
# These are the same for everyone on the team
DATABASE_URL=postgres://user:pass@host/db # @shared
REDIS_URL=redis://host:6379 # @shared
# These are personal — each developer sets their own
AWS_ACCESS_KEY_ID=AKIA...
STRIPE_SECRET_KEY=sk_test_...Or configure sharing rules globally in .envshare.config.json:
{
"sharedPatterns": ["*_URL", "*_HOST", "DB_*"],
"ignoredKeys": ["NODE_ENV", "PORT"]
}| Variable | Required | Default | Description |
|---|---|---|---|
MASTER_ENCRYPTION_KEY |
yes | — | 64-char hex. Wraps all project keys. Back this up. |
JWT_SECRET |
yes | — | 64-char hex. Signs access tokens. |
DATABASE_URL |
yes | — | PostgreSQL connection string. |
POSTGRES_PASSWORD |
yes | — | DB password (used by Docker Compose). |
ALLOWED_ORIGINS |
yes | — | Comma-separated CORS origins, e.g. https://app.com. |
PORT |
no | 3000 |
Port the backend listens on inside Docker. |
NODE_ENV |
no | production |
Set to development to enable verbose error responses. |
LOG_LEVEL |
no | info |
Winston log level (debug, info, warn, error). |
AUDIT_LOG_RETENTION_DAYS |
no | 365 |
Days before audit log entries are automatically purged. |
TRUST_PROXY |
no | false |
Set to 1 when behind a trusted reverse proxy (Caddy, nginx). |
TOKEN_CLEANUP |
no | true |
Set to false to disable automatic expired-token cleanup. |
These files are created on developer machines — they should not be committed to version control.
| File | Location | Purpose |
|---|---|---|
config.json |
~/.config/envshare-nodejs/ |
Stores the API URL and auth tokens. |
.envshare.json |
Project root | Links the directory to a project ID. Add to .gitignore. |
.envshare.config.json |
Project root | Optional push config — shared patterns, ignored keys. |
erDiagram
User {
string id PK
string email
string passwordHash
string name
datetime consentedAt
int failedLoginAttempts
datetime lockedUntil
}
Project {
string id PK
string name
string slug
string encryptedKey
}
ProjectMember {
string projectId FK
string userId FK
enum role
}
Environment {
string id PK
string projectId FK
string name
string filePath
}
Secret {
string id PK
string projectId FK
string environmentId FK
string keyHash
string encryptedKey
boolean isShared
string sharedEncryptedValue
int version
}
UserSecretValue {
string secretId FK
string userId FK
string encryptedValue
}
SecretVersion {
string secretId FK
string userId FK
string action
int version
}
AuditLog {
string actor
string action
string resourceType
string resourceId
datetime createdAt
}
User ||--o{ ProjectMember : ""
Project ||--o{ ProjectMember : ""
Project ||--o{ Environment : ""
Project ||--o{ Secret : ""
Environment |o--o{ Secret : ""
Secret ||--o{ UserSecretValue : ""
User ||--o{ UserSecretValue : ""
Secret ||--o{ SecretVersion : ""