Skip to content

Changelog

Changelog Policy

All changes to SpeedPy's public API surface — REST endpoints, serializer fields, scopes, auth behavior, webhook payloads, error shapes, rate limits, MCP schemas, and client examples — must be recorded in this changelog alongside other release notes.

Change Categories

Use these labels to classify each entry:

Category When to use
Breaking Removing/renaming fields or endpoints, changing types, tightening requirements, removing auth methods
Additive New endpoints, new optional fields, new query parameters, new enum values, new HTTP methods
Behavior change Changed semantics, different error codes, modified rate limits, altered sorting/filtering defaults
Deprecation Marking endpoints or fields for future removal (include sunset date)
Security Auth changes, permission fixes, vulnerability patches affecting integrations
Docs-only OpenAPI description updates, example corrections with no runtime effect
Experimental New features behind feature flags or explicitly marked unstable — no stability guarantee

Required Fields

Each API change entry must include:

  • Date or version — when the change ships
  • Category — one of the labels above
  • Affected surface — endpoints, fields, scopes, webhook events, or schemas involved
  • Description — what changed and why
  • Migration notes — what consumers need to do (for breaking changes and deprecations)
  • Client impact — how generated clients, webhook consumers, or integrations are affected
  • Deprecation/sunset window — when the old behavior will be removed (if applicable)
  • Links — to related schema changes, API versioning docs, or migration guides

Entry Template

### YYYY-MM-DD — `vX.Y.Z`

#### API Changes

- **Breaking:** `GET /api/v1/example/` — Removed the `legacy_field` response field.
  Consumers must use `new_field` instead. See [migration guide](./link).
  Sunset: 2026-09-01.

- **Additive:** `POST /api/v1/widgets/` — New optional `color` request field.
  No action required. Generated clients should regenerate to pick up the new field.

- **Deprecation:** `GET /api/v1/old-endpoint/` — Deprecated in favor of
  `/api/v1/new-endpoint/`. Returns `Deprecation: true` and `Sunset` headers.
  Will be removed after 2026-12-01. See [API Versioning](api-versioning.md).

- **Behavior change:** `GET /api/v1/items/` — Default sort changed from
  `created_at` to `updated_at`. Pass `?ordering=created_at` to preserve old behavior.

- **Security:** `POST /api/v1/auth/token/` — Rate limit tightened from 60/min to
  20/min per IP. Clients making bulk token requests may need to add backoff.

- **Additive:** New OAuth scope `teams:billing`. Grants read access to team
  billing data via `GET /api/v1/teams/{id}/billing/`. Existing tokens are
  unaffected; clients must request the new scope explicitly.

- **Breaking:** `POST /api/v1/widgets/` — Error response shape changed from
  `{"detail": "..."}` to `{"errors": [{"field": "...", "message": "..."}]}`.
  Clients parsing error responses must update their error-handling logic.

- **Additive:** New webhook event `invoice.finalized`. Fired when an invoice
  moves to finalized status. Payload includes `invoice_id`, `team_id`, and
  `amount`. No action required for existing webhook consumers.

- **Experimental:** `GET /api/v1/beta/suggestions/` — New endpoint, marked
  experimental. May change without notice in future releases.

Codegen-Sensitive Changes

Additive changes can break generated API clients if operation IDs or schema shapes change unexpectedly. When adding fields, endpoints, or enum values, note whether clients need to regenerate. Webhook payload changes follow the same policy as REST API response changes.

Experimental Endpoints

Endpoints marked experimental carry no stability guarantee. Document them in the changelog when added, but make clear they may change or be removed without a deprecation window.

For versioning rules, deprecation headers, and support windows, see API Versioning & Deprecation.


August 2026

Media Storage: optional S3-compatible backends

Category: Additive

Uploaded media can now live on any S3-compatible provider — AWS S3, DigitalOcean Spaces, Cloudflare R2, Wasabi, Backblaze B2, MinIO. Local disk remains the default, so existing projects are unaffected until they opt in.

  • New speedpycom/storages.py with PublicMediaStorage (media/ prefix, plain URLs, optional CDN) and PrivateMediaStorage (private/ prefix, signed URLs).
  • USE_S3=True plus the S3_* variables switches STORAGES["default"]. The provider is selected purely by S3_ENDPOINT_URL; nothing is vendor-specific.
  • New optional dependency group: uv sync --extra s3. boto3 is large, so default installs stay lean. Django resolves storage backends lazily by dotted path, so nothing imports django-storages while USE_S3 is off.
  • USE_S3=True without key, secret and bucket now refuses to boot instead of failing on the first upload.
  • New manage.py check_storage round-trips both backends against the real bucket, including asserting that a private object is refused without a signature.
  • New project.media.private_storage callable returns the right backend in either mode, so a FileField works before and after the switch with no migration.
  • New PRIVATE_MEDIA_ROOT, defaulting outside MEDIA_ROOT — anything under MEDIA_ROOT is served by the web server, so private files cannot live there.
  • Static files deliberately stay on WhiteNoise in both modes.

Full guide: Media Storage. ACLs are the one non-portable part: S3_DEFAULT_ACL defaults to empty because AWS buckets created since April 2023 have ACLs disabled and Cloudflare R2 has none, and both reject an upload carrying one.

MEDIA_URL read the wrong environment variable

Category: Behavior change

Settings read MEDIA_PATH for the media URL, a name no platform sets. On Appliku, a volume derives <PREFIX>_ROOT and <PREFIX>_URL from its environment-variable prefix, so a volume with a non-default web path was silently ignored and MEDIA_URL always fell back to /media/. It looked correct only because that fallback happens to match the usual volume path.

MEDIA_URL is now read under its real name and normalized in project/media.py, which also handles the platform emitting <PREFIX>_URL as the literal string "None" when a volume has no web path, and guarantees the trailing slash Django requires.

Action: if you set MEDIA_PATH in your own environment, rename it to MEDIA_URL.

Anonymous requests returned 500 on every team URL

Category: Security

TeamViewMixin.dispatch resolved the team and queried TeamMembership before calling super().dispatch(), which is where LoginRequiredMixin performs its check. A logged-out request therefore reached the membership query with AnonymousUser and raised ValidationError: "AnonymousUser" is not a valid UUID, producing a 500 on every team-scoped URL instead of a redirect to the login page.

Because the team lookup ran first, the response also distinguished a real team id (500) from an unknown one (404) without authentication. Team ids are UUID4 and cannot be guessed, so this was a weak signal rather than enumeration — but it is closed now: authenticated redirect happens before any database read, so both cases return an identical 302.

No data was exposed; the request died before any team content was fetched.

Post-checkout activation no longer races the provider webhook

Category: Behavior change · Affected surface: billing checkout, new …/billing/activation/ endpoint

Paddle redirects the browser the instant payment succeeds, while its webhook is a separate server-to-server call that lands afterwards. The billing page therefore showed the customer their old plan until they refreshed by hand.

  • On checkout.completed, the page now posts the transaction id to a new owner-only …/billing/activation/ endpoint, which reads the subscription from the provider API and applies it through the same idempotent path the webhook uses. The plan is correct on first paint, and a webhook that is delayed or lost entirely now self-heals.
  • If that cannot confirm, the page renders an explicit "Activating" state and polls the same endpoint with backoff, then reports plainly if it takes too long.
  • settings.successUrl is no longer sent, so the provider cannot redirect out from under the provisioning call. A client-side timeout prevents a stalled request from stranding the customer on the overlay.
  • Cross-tenant guard: the billable is resolved from the provider's signed custom data and compared with the caller's account before anything is applied.
  • BillingAdapter.fetch_subscription_state() is the new provider seam. Adapters that do not implement it return None and the page falls back to polling, so Stripe is unaffected.
  • Paddle checkout failures now surface the provider's code/detail instead of an opaque "Something went wrong".

Tests

  • The team-invitation webhook tests no longer open a real AMQP broker connection. They execute on_commit callbacks, and the invitation email is queued through current_app.send_task, which was not patched.

March 2026

Django 6.0

SpeedPy has been updated to Django 6.0.3, a major version upgrade from Django 5.2.8.

Django 6.0 brings continued improvements to the ORM, async support, and template engine. Review the Django 6.0 release notes before merging this update into your project, as there may be breaking changes depending on which Django internals you use.

Infrastructure

  • Node.js updated from 25.6.0 to 25.8.1 in the Docker image

Python Package Updates

Package Previous Updated
Django 5.2.8 6.0.3
django-stubs 5.2.8 6.0.1
gunicorn 23.0.0 25.1.0
django-ses 4.5.0 4.7.2
djangoql 0.18.1 0.19.1
pyjwt[crypto] 2.10.1 2.12.1
djangorestframework 3.16.1 3.17.0
django-otp 1.6.3 1.7.0
django-allauth 65.13.1 65.15.0
django-crispy-forms 2.5 2.6
django-debug-toolbar 6.1.0 6.2.0
django-environ 0.12.0 0.13.0
celery[redis] 5.6.1 5.6.2
whitenoise 6.11.0 6.12.0
psycopg 3.3.2 3.3.3
Pillow 12.0.0 12.1.1
black 25.12.0 26.3.1

New: update_pyproject.py script

A new update_pyproject.py script is now included in the boilerplate. It automates keeping your Python dependencies up to date by fetching the latest versions from PyPI and rewriting pyproject.toml in place.

How to get this update

Pull it into your project via the speedpy Git remote:

git fetch speedpy
git merge speedpy/master --allow-unrelated-histories

See the Updating page for full instructions.