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].