GreenCoast
A privacy-first, shardable social backend + minimalist client. Zero PII, zero passwords, optional E2EE per post, and public-key accounts. Includes DPoP-style proof-of-possession, Discord SSO with PKCE, and a tiny static client.
Features
- Zero-trust by design: server stores no emails or passwords.
- Accounts = public keys (Ed25519 or P-256). No usernames required.
- Proof-of-possession (PoP) on every authenticated API call.
- Short-lived tokens (HMAC “gc2”) bound to device keys.
- Shardable storage (mTLS or signed shard requests).
- No fingerprinting: no IP/UA logs; coarse timestamps optional.
- Static client with strong CSP; optional E2EE per post.
- Discord SSO (PKCE) as an optional convenience.
- Filesystem storage supports both flat and nested object layouts.
Architecture (brief)
- Shard: stateless API + local FS object store + in-memory index.
- Client: static files (HTML/JS/CSS) served by the shard or any static host.
- Identity: device key (P-256/Ed25519) or passkey; server mints short-lived gc2 tokens bound to the device key (
cnf
claim). - Privacy: objects can be plaintext (public) or client-encrypted (private).
Security posture
- Zero-trust: no passwords/emails; optional SSO is linking, not source-of-truth.
- DPoP-style PoP on requests:
- Client sends:
Authorization: Bearer gc2.…
X-GC-Key: p256:<base64-raw>
(ored25519:…
)X-GC-TS: <unix seconds>
X-GC-Proof: sig( METHOD "\n" URL "\n" TS "\n" SHA256(body) )
- Server verifies
gc2
signature, key binding (cnf
), timestamp window, and replay cache.
- Client sends:
- Replay protection: 10-minute proof cache.
- No fingerprinting/logging: no IPs, no UAs.
- Strict CSP for client: blocks XSS/token theft.
- Limits: request body limits (default 10 MiB), simple per-account rate limiting.
- Shard↔shard: mTLS or per-shard signatures with timestamp + replay cache.
Requirements
- Go 1.21+
- Docker (optional)
- A signing key for tokens:
GC_SIGNING_SECRET_HEX
(32+ bytes hex) - (Optional) Discord OAuth app (Client ID/Secret + redirect URI)
- (Optional) Cloudflare Tunnel or other TLS reverse proxy
Environment variables
GC_HTTP_ADDR=:9080
GC_HTTPS_ADDR= # optional
GC_TLS_CERT= # optional
GC_TLS_KEY= # optional
GC_STATIC_ADDR=:9082
GC_STATIC_DIR=/opt/greencoast/client
GC_DATA_DIR=/var/lib/greencoast
GC_ZERO_TRUST=true
GC_COARSE_TS=false
GC_SIGNING_SECRET_HEX=<64+ hex chars> # required for gc2 tokens
GC_REQUIRE_POP=true # default true; set false for first-run
# Dev convenience (testing only; disable for production)
GC_DEV_ALLOW_UNAUTH=false
GC_DEV_BEARER=
# Discord SSO (optional)
GC_DISCORD_CLIENT_ID=
GC_DISCORD_CLIENT_SECRET=
GC_DISCORD_REDIRECT_URI=https://greencoast.example.com/auth-callback.html
Quickstart (Docker)
Minimal compose for local testing (PoP disabled + dev unauth allowed for first run):
services:
shard-test:
build: .
environment:
- GC_HTTP_ADDR=:9080
- GC_STATIC_ADDR=:9082
- GC_STATIC_DIR=/opt/greencoast/client
- GC_DATA_DIR=/var/lib/greencoast
- GC_ZERO_TRUST=true
- GC_SIGNING_SECRET_HEX=7f6e1a0f2b4d7e3a... # replace with your secret
- GC_REQUIRE_POP=false # easier first-run
- GC_DEV_ALLOW_UNAUTH=true
volumes:
- ./testdata:/var/lib/greencoast
- ./client:/opt/greencoast/client:ro
ports:
- "9080:9080"
- "9082:9082"
Open http://localhost:9082
→ set the Shard URL (http://localhost:9080
) → publish a test post.
When ready, turn PoP on by removing GC_REQUIRE_POP=false
and disabling GC_DEV_ALLOW_UNAUTH
.
Cloudflare Tunnel example
ingress:
- hostname: greencoast.example.com
service: http://shard-test:9082
- hostname: api-gc.greencoast.example.com
service: http://shard-test:9080
- service: http_status:404
Use “Full (strict)” TLS and ensure your cert covers both hosts.
Client usage
- Shard URL: set it in the top “Connect” section (or use
?api=
query or<meta name="gc-api-base">
). - Device key sign-in (no OAuth):
- Client generates/stores a P-256 device key in the browser.
- Client calls
/v1/auth/key/challenge
then/v1/auth/key/verify
to obtain a gc2 token bound to that key.
- Discord SSO (optional):
- Requires
GC_DISCORD_CLIENT_*
env vars and a validGC_DISCORD_REDIRECT_URI
. - Uses PKCE (
S256
) and binds the minted gc2 token to the device key presented at/start
.
- Requires
API (overview)
GET /healthz
– livenessPUT /v1/object
– upload blob (headers: optionalX-GC-Private: 1
,X-GC-TZ
)GET /v1/object/{hash}
– download blobDELETE /v1/object/{hash}
– delete blobGET /v1/index
– list indexed entries (latest first)GET /v1/index/stream
– SSE updatesPOST /v1/admin/reindex
– rebuild index from disk- Auth
POST /v1/auth/key/challenge
→{nonce, exp}
POST /v1/auth/key/verify
{nonce, alg, pub, sig}
→{bearer, sub, exp}
POST /v1/auth/discord/start
(requiresX-GC-3P-Assent: 1
andX-GC-Key
)GET /v1/auth/discord/callback
→ redirects with#bearer=…
- GDPR
GET /v1/gdpr/policy
– current data-handling posture
When
GC_REQUIRE_POP=true
, all authenticated endpoints require PoP headers.
PoP header format (pseudocode)
Authorization: Bearer gc2.<claims>.<sig>
X-GC-Key: p256:<base64-raw> # or ed25519:<base64-raw>
X-GC-TS: <unix seconds>
X-GC-Proof: base64(
Sign_device_key(
UPPER(METHOD) + "\n" + URL + "\n" + X-GC-TS + "\n" + SHA256(body)
)
)
Storage layout & migration
- Writes are flat:
objects/<hash>
- Reads (and reindex) also support:
objects/<hash>/blob|data|content
objects/<hash>/<single file>
objects/<prefix>/<hash>
(two-level prefix)
- To restore data into a fresh container:
- Mount your objects at
/var/lib/greencoast/objects
- Call
POST /v1/admin/reindex
(with auth+PoP or enable dev unauth briefly)
- Mount your objects at
Reindex examples
Unauth (dev only):
curl -X POST https://api-gc.yourdomain/v1/admin/reindex
With bearer + PoP (placeholders):
curl -X POST https://api-gc.yourdomain/v1/admin/reindex ^
-H "Authorization: Bearer <gc2_token>" ^
-H "X-GC-Key: p256:<base64raw>" ^
-H "X-GC-TS: <unix>" ^
-H "X-GC-Proof: <base64sig>"
Hardening checklist (prod)
- Set
GC_REQUIRE_POP=true
, remove dev bypass. - Keep access token TTL ≤ 8h; rotate signing key periodically.
- Static client served with strong CSP (already enabled).
- Containers run non-root, read-only FS,
no-new-privileges
,cap_drop: ["ALL"]
. - Edge WAF/rate limits; 10 MiB default request cap (tunable).
- Commit
go.sum
; rungo mod verify
in CI.
GDPR
- Server stores no PII (no emails, no IP/UA logs).
- Timestamps are UTC (or coarse UTC if enabled).
/v1/gdpr/policy
exposes current posture.- Roadmap:
/v1/gdpr/export
and/v1/gdpr/delete
to enumerate/remove blobs signed by a given key.
License
This project is licensed under The Unlicense. See LICENSE
for details.