Trust & Security
Skua is designed for sharing work outputs – charts, tables, analysis results. Here's exactly how we protect that data, what our limits are, and what you should think about before uploading.
The right mindset
Skua is a sharing tool, not a vault. It's built for the outputs of your work – the chart you'd put in a slide deck, the table you'd paste into Slack. If you wouldn't email it to a colleague, don't record it.
We take security seriously and have built real protections (detailed below), but we also believe in being straight with you: any time you put data on a third-party service, you're trusting that service. We want to earn that trust by being transparent about what we do and don't do.
Access model
Every record has one of three visibilities. The first two share a capability-based access model: the URL is the credential, like Google Docs "anyone with the link" or Figma share links. The third is authenticated-only.
- Public – appears on your profile and anyone with the URL can view it. Default for anonymous users; also the default for verified users.
- Unlisted – hidden from your profile but anyone with the URL can view. Useful for sharing with specific people without broadcasting. Verified accounts only.
- Private – only the author can view. The server checks your authentication cookie or CLI token on every request; anyone else gets a 404, even with the URL. Verified accounts only.
So public and unlisted share the "URL-based access" tradeoff: if a URL leaks, the content is visible to whoever has it. Private records do not — losing a URL doesn't expose the content because the URL alone isn't enough.
For public and unlisted records, don't share the URL with people you wouldn't want to see the content.
URLs are unguessable
Every record gets a 12-character ID generated using secrets.choice() from Python's cryptographic random module. The alphabet is base62 (a–z, A–Z, 0–9), giving each
ID 71.5 bits of entropy – over 3 sextillion possible
combinations.
IDs have no sequential or time-based component. Knowing one ID tells you nothing about any other. At the rate of 1 billion guesses per second, it would take over 100,000 years to find a single valid ID by brute force – and our rate limiting would block you long before that. This matters most for public and unlisted records; private records are additionally protected by authentication.
Authentication
Your CLI token lives on your machine — ~/.skua/client for anonymous
installs, ~/.skua/token once you verify. The
SDK sends the token via an HTTP header on every request; no cookies are set from the
Python client.
The web UI uses a separate HttpOnly skua_session cookie, set when you
click an email verification link or a one-click login URL from skua.open_profile(). It's scoped to
the browser and expires in 30 days.
- Tokens are hashed with SHA-256 before storage – the server never stores your raw token
- Email verification tokens are single-use and expire after 15 minutes
Encryption
In transit: all connections use HTTPS/TLS with Let's Encrypt certificates. HTTP requests are redirected to HTTPS. HSTS is enabled with a 1-year max-age. HTTP/2 is supported.
At rest: record data and the PostgreSQL database live on encrypted block storage on a dedicated Hetzner VPS in Ashburn, Virginia (US East). We do not currently offer client-side encryption – data is encrypted by the infrastructure, not by the application.
Security headers are set on all responses: X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security.
Rate limiting & abuse prevention
Two layers of rate limiting protect the service:
- Request rate limits – 60 uploads per minute, 200 per hour (burst-friendly for notebook "Run All" workflows). Read endpoints allow 1,000 requests per hour.
- Failure-based blocking – 20 failed requests (404s, invalid IDs) in 5 minutes triggers an automatic IP block with escalating durations: 10 minutes, 1 hour, then 24 hours for repeat offenders.
Nginx enforces a 10 MB upload cap and a 10 requests/second rate limit before traffic reaches the application.
Data retention
- Anonymous: records expire after 30 days
- Verified: 365-day retention, resetting on each update
An automated cleanup process runs hourly. When a record expires, both the database row and the stored file are permanently deleted – storage first, then database, so no orphaned files remain accessible.
What we collect
No tracking cookies. No telemetry from the Python package. No user agents or referrers logged. Website analytics via Fathom (privacy-focused, cookieless, GDPR compliant).
What we do collect – the minimum needed to operate:
- Your uploaded content – exactly what you pass to
skua.record() - IP address – for rate limiting and abuse prevention only, not linked to identity
- Email – used only for verification, and for product updates if you opt in
- Metadata – timestamps, content sizes, tags
Input validation
All input is validated at multiple layers – the Python client validates locally for fast
feedback, and the backend re-validates everything with Pydantic models regardless of what
the client sends. Database queries use parameterized statements via SQLAlchemy (no string
interpolation, no SQL injection risk). CSV exports sanitize cells that start with formula
characters (=, +, @) to prevent injection in spreadsheet
applications.
Open source client
The Python package is open source (MIT License). You can read exactly what data the client sends, how it serializes your objects, and what HTTP requests it makes. There are no hidden analytics, no phone-home calls, no data collection beyond what's needed to upload your record.
Our recommendation
Skua is a good fit for:
- Charts and visualizations you'd present to your team
- DataFrames and tables with non-sensitive business data
- Analysis results, model outputs, experiment summaries
- Anything you'd put in a slide deck or Slack message
Think twice before uploading:
- PII, health records, or financial data covered by regulation
- Credentials, API keys, or secrets
- Proprietary data your employer restricts from third-party services
For public and unlisted records, the right test is: "Would I be comfortable if this URL were forwarded to someone I didn't intend?" If yes, record it. If not, use private or keep it local.
Continuity
Skua is built and operated by one person. If Skua ever shuts down, I'll give at least 30 days notice and keep read access available during that window so you can save anything you need. The Python client is open source and your records are always accessible via their URLs for as long as the service runs.
Security questions or concerns? Contact [email protected].