Architecture¶
Technical architecture and design decisions.
Design Principles¶
- Interface-based DI: All external dependencies behind interfaces
- Testability: Mocks for all interfaces
- Separation of concerns: CLI, business logic, and infrastructure are separate
Core Interfaces¶
Storage¶
type Storage interface {
Upload(ctx context.Context, path string, r io.Reader) error
Download(ctx context.Context, path string) (io.ReadCloser, error)
List(ctx context.Context, prefix string) ([]string, error)
Delete(ctx context.Context, path string) error
Exists(ctx context.Context, path string) (bool, error)
}
Encrypter¶
type Encrypter interface {
Encrypt(plaintext []byte) ([]byte, error)
Decrypt(ciphertext []byte) ([]byte, error)
}
Repository¶
type Repository interface {
Init() error
Add(paths ...string) error
Commit(message string) (string, error)
Log(n int, includeFiles bool) ([]Commit, error)
Checkout(ref string) error
ListFiles() ([]string, error)
ReadFile(path, ref string) ([]byte, error)
WriteFile(path string, content []byte) error
RemoveFile(path string) error
Head() (string, error)
PackAll(w io.Writer) error
UnpackAll(r io.Reader) error
GetAllRefs() (map[string]string, error)
SetRef(name, hash string) error
DeleteRef(name string) error
}
Package Dependencies¶
cmd/envsecrets
└── internal/cli
├── internal/config
├── internal/sync
│ ├── internal/storage
│ ├── internal/crypto
│ ├── internal/git
│ └── internal/cache
├── internal/project
└── internal/ui
Data Flow¶
Push¶
- CLI parses flags and loads config
- Project discovery finds repo identity and env files
- Sync from GCS: download packfile + refs, restore full git history locally
- Fast-forward local branch to remote HEAD if behind
- For each file:
- Read plaintext from project directory
- Encrypt with age
- Write encrypted file to cache
- Commit changes to cache git repo
- Conflict check: verify remote HEAD hasn't changed since step 3
- Sync to GCS: create packfile of all objects + refs, upload to GCS
Pull¶
- CLI parses flags and loads config
- Project discovery finds repo identity
- Sync from GCS: download packfile + refs, restore full git history locally
- Checkout requested ref (or HEAD) to populate working tree
- For each file in cache:
- Read encrypted file
- Decrypt with age
- Write plaintext to project directory
Cache Structure¶
~/.envsecrets/
├── config.yaml
└── cache/
└── {owner}/
└── {repo}/
├── .git/ # Full git history (restored from packfile)
├── .env.age # Encrypted files (working tree, populated by checkout)
└── .env.local.age
The cache is a git repository containing only encrypted files. Full git history is synced between machines via packfiles stored in GCS.
GCS Storage Layout¶
{owner}/{repo}/objects.pack # Packfile containing all git objects
{owner}/{repo}/refs # Text file: refname SP hash LF
{owner}/{repo}/HEAD # Current HEAD commit hash
Every sync downloads the packfile and restores full git history locally. This
enables log, diff, and revert to work correctly across machines with
shared commit history.
Error Handling¶
Sentinel errors in internal/domain/errors.go map to exit codes:
| Error | Exit Code | Description |
|---|---|---|
| ErrNotConfigured | 1 | Missing configuration |
| ErrNotInRepo | 2 | Not in a git repository |
| ErrNoEnvFiles | 3 | No .envsecrets file found |
| ErrConflict | 4 | Local/remote conflict |
| ErrDecryptFailed | 5 | Decryption failed |
| ErrUploadFailed | 6 | GCS upload failed |
| ErrDownloadFailed | 7 | GCS download failed |
Configuration Loading¶
- Check
--configflag - Check
ENVSECRETS_CONFIGenvironment variable - Use default
~/.envsecrets/config.yaml - Validate required fields
- Resolve passphrase (env, command, or prompt)