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.pywithPublicMediaStorage(media/prefix, plain URLs, optional CDN) andPrivateMediaStorage(private/prefix, signed URLs). USE_S3=Trueplus theS3_*variables switchesSTORAGES["default"]. The provider is selected purely byS3_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 whileUSE_S3is off. USE_S3=Truewithout key, secret and bucket now refuses to boot instead of failing on the first upload.- New
manage.py check_storageround-trips both backends against the real bucket, including asserting that a private object is refused without a signature. - New
project.media.private_storagecallable returns the right backend in either mode, so aFileFieldworks before and after the switch with no migration. - New
PRIVATE_MEDIA_ROOT, defaulting outsideMEDIA_ROOT— anything underMEDIA_ROOTis 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.successUrlis 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 returnNoneand the page falls back to polling, so Stripe is unaffected.- Paddle checkout failures now surface the provider's
code/detailinstead of an opaque "Something went wrong".
Tests¶
- The team-invitation webhook tests no longer open a real AMQP broker connection.
They execute
on_commitcallbacks, and the invitation email is queued throughcurrent_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:
See the Updating page for full instructions.