Skip to main content

Per-org storage configuration

Every tenant org stores its own uploads — chapter logos, member avatars, forum attachments, photo albums, exported JSON, learning artifacts — in an object-storage bucket. Most tenants point at their own AWS S3, MinIO, or S3-compatible bucket so they own their data, encryption keys, and lifecycle. As the platform admin you can view, set, override, and test that configuration on a tenant's behalf — useful for onboarding, migration, and incident recovery.

When the platform admin sets storage

In normal operation, an org's national admin configures their own storage from the org settings page. You step in as platform admin when:

  • Onboarding a new tenant — the org's admins want help wiring up credentials the first time
  • Recovering a misconfigured tenant — they saved bad credentials, can't authenticate anymore, and you need to fix the value
  • Provisioning shared platform-default storage — the tenant doesn't have their own bucket and you're putting them on the platform-default MinIO/S3 with a path prefix
  • Migrating a tenant between providers — set the new credentials, run a connectivity test, then trigger a storage migration to copy files across

If a tenant has neither their own storage config nor an explicit assignment from you, the application falls back to the platform-default storage configured at the platform level.

Open per-org storage

Platform → Organizations, pick the tenant, then Storage in the per-org navigation.

Platform per-org Storage tab with provider, bucket, region, and credential fields. Platform per-org Storage tab with provider, bucket, region, and credential fields.

You'll see (if any config exists):

  • Provider — MinIO, AWS S3, or S3-compatible
  • Endpoint URL — required for MinIO and S3-compatible; blank for AWS S3
  • Bucket name — must already exist; the platform will not auto-create
  • Access key ID — IAM credential ID
  • Secret access key — encrypted at rest; displayed only as a placeholder once saved
  • Region — defaults to us-east-1
  • Custom domain — optional CDN/CloudFront hostname for public asset URLs
  • Use SSL — keep on
  • Path prefix — optional; useful when multiple orgs share one bucket (e.g., orgs/alpha/)
  • Querystring auth — toggles signed-URL auth vs. public-read for assets
  • Is active — turn off to fall back to platform-default without deleting the config
  • Is verified — read-only; flips to true after a successful connection test

If no config exists for the tenant, the form is empty and saving creates a new record.

Saving credentials

Fields are validated client-side, then sent to the platform API. The secret access key is encrypted at rest with the platform's Fernet key the moment the row is written — it's never stored in plaintext.

Saving sets Is verified = false. The application will not actually use a storage config until it's verified, which means a fresh save always needs the next step.

Connection test

Click Test connection after saving. The platform builds a boto3 client from the saved config, uploads a tiny probe file to .greekmanage-test/<org-id>/connectivity-test.txt, verifies it exists, then deletes it. On success, Is verified flips to true and the storage cache for that org is invalidated so the new config takes effect immediately.

A failed test surfaces a generic "Connection test failed. Check your configuration." message. The detailed reason is logged server-side but kept out of the response intentionally (storage error messages can leak bucket structure). Common failure modes to walk through:

  • Wrong region. AWS S3 returns AuthorizationHeaderMalformed when the signing region doesn't match the bucket region. Pick the bucket's actual region (us-east-2, eu-west-1, etc.), not the IAM user's home region.
  • Signature mismatch. Usually an incorrect or copy-paste-truncated secret access key. Rotate the key in IAM and try again.
  • CORS missing or wrong. Direct-from-browser uploads (photo albums, large attachments) need a CORS policy on the bucket that allows PUT, GET, POST, DELETE and HEAD from your platform domain. The probe upload doesn't go through the browser so it'll pass even with broken CORS, but live uploads from the app will fail.
  • Endpoint URL wrong for provider. AWS S3 endpoints look like https://s3.us-east-1.amazonaws.com; MinIO is your self-hosted URL; Backblaze B2 / Wasabi / DO Spaces all have their own. Leave Endpoint URL blank for AWS S3.
  • Bucket doesn't exist. Create it in the provider console first; the platform does not auto-create.
  • IAM policy too restrictive. The probe needs s3:PutObject, s3:GetObject, s3:DeleteObject, and s3:ListBucket at minimum.
  • SSL/TLS issues. Toggle Use SSL to match the endpoint (https → on, http → off). Self-hosted MinIO behind a reverse proxy without TLS needs SSL off.

Until the test succeeds, the storage stays unverified and the app keeps using whatever was active before (often the platform-default fallback).

Switching a tenant from platform-default to their own bucket

This is the migration path used most often during onboarding:

  1. Tenant creates their bucket and IAM user with the policy above.
  2. Tenant gives you (or their org admin gives them) the access key and secret.
  3. Save the credentials on the per-org Storage tab.
  4. Run Test connection until it succeeds.
  5. Trigger a storage migration to copy existing assets (chapter logos, member avatars, photo albums, attachments) from the previous storage to the new one.
  6. Once migration completes, new uploads automatically land in the new bucket — the migration task flips active storage atomically at the end.

The migration runs as a Celery task and records its progress (total_files, files_copied, files_failed, error_log) for audit. A migration that finishes with failures stays in failed status until someone reviews the error log and re-runs.

Encryption

The secret access key is encrypted at rest using the platform's Fernet key, the same encryption that protects payment processor credentials and AI provider keys. Even a database snapshot leak would not expose the value in plaintext — the attacker would also need the Fernet key, which lives in environment configuration.

Rotating the Fernet key is a separate platform operation (see the secrets rotation runbook); after rotation, encrypted fields re-encrypt automatically with the new key.

Caching

The platform caches each org's active storage config in memory to avoid hitting the database on every upload. The cache is invalidated automatically when you save credentials or pass a connection test — no manual cache-flush step needed. If you ever suspect stale caching, restart the backend workers.

What's NOT in the box

  • No bucket auto-create. Always create the bucket on the provider side first.
  • No IAM policy generator. The policy is in the storage-config doc for org admins; copy it into the IAM console.
  • No automatic file-count probe. The connection test verifies access; it does not inventory the bucket.
  • No multi-bucket per org. A tenant has exactly one storage config at a time.
  • No diff/rollback for storage credentials. Re-saving overwrites the previous credentials (and the secret can never be read back). Keep the prior secret somewhere safe until the new one is verified.

Tips

  • Always run Test connection right after saving. An unverified config is silently inert.
  • Re-test after changing CORS or the bucket region. Those don't go through the storage-config form and the cached verified state doesn't notice.
  • Use a path prefix when sharing a bucket across tenants. Even if you don't think you'll share, it's free insurance.
  • Document the IAM user per tenant. "Which IAM user is currently signed in to GreekManage for Alpha Tau Omega?" is a question that comes up during incident response; the platform doesn't track it for you.

Last verified against v0.62.1 (2026-05-11).