plain changelog
0.124.0 (2026-03-12)
What's changed
- Updated all references from
plain.modelstoplain.postgresacross views, CLI docs, registry docstrings, README doc links, and agent rules.
Upgrade instructions
- Update imports:
from plain.modelstofrom plain.postgres,from plain import modelstofrom plain import postgres.
0.123.4 (2026-03-12)
What's changed
- Read cgroup v2 CPU quota for accurate container worker count —
os.process_cpu_count()only checkssched_getaffinity, not cgroup CPU quotas, so containers still saw the host CPU count. Now reads/proc/self/cgroupto resolve the process's cgroup path, then parsescpu.maxfor the actual quota. Uses ceiling division so fractional vCPUs (e.g. 1.5) round up to 2 workers. Silently falls through on non-Linux systems (32785b4634e8)
Upgrade instructions
- No changes required.
0.123.3 (2026-03-12)
What's changed
- Fix auto worker count in Docker/cgroup environments — replaced
os.cpu_count()withos.process_cpu_count()(Python 3.13+) for cgroup-aware CPU detection. Previously, containers would see the host's CPU count instead of their allocated limit, spawning far too many workers (e.g. 48 on a 2 vCPU container) (c1e2c186c3aa)
Upgrade instructions
- No changes required.
0.123.2 (2026-03-12)
What's changed
- Server startup log now includes workers and threads — the startup message shows
workers=N threads=Nso you can confirm the server configuration at a glance (e63d9f90520c) - H2 max concurrent streams setting registered with default of 100 —
SERVER_H2_MAX_CONCURRENT_STREAMSis now a proper setting with a default value instead of usinggetattrwith a fallback (e72a4006515f)
Upgrade instructions
- No changes required.
0.123.1 (2026-03-12)
What's changed
- Health check moved from middleware to server event loop — the
HEALTHCHECK_PATHendpoint now responds directly on the async event loop with a raw200 OKbefore the request reaches the thread pool or any middleware. This means health checks continue to work even when the thread pool is fully saturated. Supports both HTTP/1.1 and HTTP/2 connections (ef8f020a86dc) - Removed
HealthcheckMiddleware— no longer needed since the server handles health checks directly (ef8f020a86dc)
Upgrade instructions
- No changes required.
0.123.0 (2026-03-11)
What's changed
- Open redirect protection for
RedirectResponse— external URLs are now rejected by default. Passallow_external=Trueto explicitly allow redirects to external hosts (OAuth, CDN, etc.). Detects scheme-based URLs (http://,https://,ftp://), protocol-relative (//), and backslash variants (/\,\\) with whitespace stripping to prevent bypass attacks (5edfb2bedf90) RedirectView.allow_externalattribute — class-based redirect views now supportallow_external = Truefor views that intentionally redirect to external URLs (5edfb2bedf90)
Upgrade instructions
If your code passes external URLs to
RedirectResponse(e.g., OAuth providers, CDN URLs, SSO login pages), addallow_external=True:# Before RedirectResponse("https://example.com/callback") # After RedirectResponse("https://example.com/callback", allow_external=True)For
RedirectViewsubclasses that redirect externally, setallow_external = Trueon the class.Relative paths, query-only URLs, and other internal redirects continue to work without changes.
0.122.1 (2026-03-11)
What's changed
- Fixed
is_boundfor forms with no fields (e.g., delete confirmation forms) — empty POST requests produced a falsyQueryDict, causing the form to never validate.is_boundnow checks the request method instead of data truthiness (f630b3b5fb22)
Upgrade instructions
- No changes required.
0.122.0 (2026-03-10)
What's changed
Secretis now type-transparent —Secret[str]is a type alias forAnnotated[str, _SecretMarker()], so type checkers see the underlying type directly. No moretype: ignorecomments needed on default values (a90197b95315, 997afd9a558f)TYPE_CHECKINGignored in settings modules — settings loading now skipsTYPE_CHECKING(and other uppercase non-setting names) so you can usefrom __future__ import annotationsandif TYPE_CHECKING:blocks in settings files without triggering duplicate-setting errors (f20869e0bd2b)- Adopted PEP 695 type parameter syntax (
def foo[T]()instead ofTypeVar) across the codebase (aa5b2db6e8ed)
Upgrade instructions
- If you previously had
type: ignore[assignment]comments onSecretdefault values, you can remove them. - No other changes required.
0.121.2 (2026-03-10)
What's changed
- Added prescriptive "After making code changes" section to AI rules with
plain checkandplain requestguidance (772345d4e1f1) - Distributed Django differences into package-specific rules (plain-models, plain-templates) instead of one monolithic list (772345d4e1f1)
- Added
request.query_params,request.form_data,request.json_data,request.filesto Django differences (772345d4e1f1)
Upgrade instructions
- No changes required.
0.121.1 (2026-03-10)
What's changed
- Worker SIGTERM exits logged at info instead of error — during graceful shutdown (e.g. Heroku deploy), workers exit with SIGTERM which is expected behavior. These are now logged at
infolevel instead oferror, preventing false alerts in error tracking (1c3908d27aea) - Removed duplicate log messages for signal-killed workers — workers killed by a signal previously produced two error log lines (generic exit code + signal name). Now only the more descriptive signal-specific message is logged (1c3908d27aea)
- Removed stack traces from intentional 4xx exception logging —
PermissionDenied,MultiPartParserError, andBadRequestError400exceptions no longer includeexc_infoin their log entries, reducing noise in error tracking (c395232acdb9) - Handle asyncio
ConnectionResetErrorwithout errno — asyncio's_drain_helperraisesConnectionResetError('Connection lost')without an errno, which bypassed the existing errno-based check. Now caught explicitly before theOSErrorerrno check (b623d4f78667) - Removed
type: ignorecomments across multiple modules with proper type fixes (cda461b1b4f6, f56c6454b164)
Upgrade instructions
- No changes required.
0.121.0 (2026-03-09)
What's changed
- Replaced
threading.local()withContextVarfor async compatibility — timezone activation (plain.utils.timezone) and URL resolver population tracking now useContextVarinstead ofthreading.local(), making them safe for use in async contexts where multiple coroutines share a thread (e5c4073cafbc) - Graceful handling of client disconnects — the server now catches
OSErrorduring keepalive waits (e.g. client TCP reset) andConnectionErrorduring connection handling, preventing noisy tracebacks from abrupt client disconnects (e3aee49b32ac) - Updated
BaseHandler._run_in_executorto propagate only the OpenTelemetry span context into executor threads, intentionally leaving DB connection ContextVars on their native thread context so connections persist across requests honoringCONN_MAX_AGE(cc2469b1260a)
Upgrade instructions
- No changes required.
0.120.1 (2026-03-08)
What's changed
- Simplified TLS handling using
asyncio.start_server(ssl=...)— TLS is now handled at connection accept time by asyncio instead of a manual two-step process (accept raw socket, then handshake). This eliminates the_async_tls_handshakemethod, thehanded_offflag, and all raw socket I/O fallback paths.Connectionnow always receivesasyncio.StreamReader/StreamWriterinstead of a raw socket, removing dual-path code inrecv,sendall,wait_readable,close, andResponse._async_send(c1e172eec835) - Removed
RequestParser,SocketUnreader,IterUnreader, and several raw socket utility functions (close,write,write_chunk,write_nonblock,async_recv,async_sendall,has_fileno,ssl_wrap_socket) that are no longer needed (c1e172eec835) - Removed synchronous
Responsewrite methods (send_headers,write,sendfile,write_file,write_response,close) and theSERVER_SENDFILEsetting — all response writing now uses the async path (c1e172eec835) - Connection backpressure now uses
asyncio.Semaphoreinstead of a manualasyncio.Eventwith count checks (c1e172eec835)
Upgrade instructions
- No changes required.
0.120.0 (2026-03-07)
What's changed
- Stalled thread pool detection in worker heartbeat — the worker now submits a no-op to the thread pool during each heartbeat cycle and waits for it to complete within the timeout window. If the thread pool is stalled (all threads blocked), the heartbeat stops, causing the arbiter to kill and restart the worker automatically (ce09f41a9db5)
Upgrade instructions
- No changes required.
0.119.0 (2026-03-07)
What's changed
- Removed
url_argsand positional URL arguments — views no longer receive positional URL arguments viaself.url_args. All URL parameters are now keyword-only viaself.url_kwargs.reverse()andreverse_absolute()no longer accept*args— use keyword arguments instead.RegexPatternnow rejects unnamed capture groups and requires named groups ((?P<name>...)) (6eecc35ff197) - Server I/O moved to async event loop — all network I/O (TLS handshakes, reading requests, writing responses, keep-alive) now runs on the asyncio event loop. The thread pool is reserved exclusively for application code (middleware and views). This improves connection handling efficiency and eliminates H2 reader threads (322fd51cf206, f60bcdd6f4d2)
- New
SERVER_CONNECTIONSsetting — controls the maximum number of concurrent connections per worker (default: 1000). Replaces implicit connection limits (322fd51cf206) - New
SERVER_H2_MAX_CONCURRENT_STREAMSsetting — optionally limits the number of concurrent HTTP/2 streams per connection (322fd51cf206) - Asyncio debug mode in development — when
DEBUG=True, the server enables asyncio debug mode which logs warnings when a callback blocks the event loop for more than 100ms, helping catch blocking calls in async views (3afdae32ef94) - Added documentation for async view safety, server request lifecycle, and three-layer architecture (3afdae32ef94, ca82fb46ad7e)
Upgrade instructions
- Replace
self.url_argswithself.url_kwargsin all views. If you used positional URL arguments with unnamed regex groups, convert them to named groups ((?P<name>...)). - Replace any
reverse(name, arg1, arg2)calls withreverse(name, param1=arg1, param2=arg2)using keyword arguments. - Replace any
reverse_absolute(name, arg1)calls similarly. - In templates, replace
url("name", arg1)withurl("name", param1=arg1)using keyword arguments. - No server configuration changes are required — the new settings have sensible defaults.
0.118.0 (2026-03-06)
What's changed
- Removed
as_view()andsetup()from View — views are now passed as classes directly to URL patterns (e.g.,path("foo/", MyView)instead ofpath("foo/", MyView.as_view())). Thesetup()method is removed; request, URL args, and URL kwargs are set directly on the view instance.View.__init__no longer accepts arguments. Theview_classattribute moved fromURLPattern.viewtoURLPattern.view_classdirectly (0d0c8a64cb45) - Removed
--pidfileoption from server — the--pidfileCLI option andSERVER_PIDFILEsetting have been removed (3ac519e691b2) - Removed
--max-requestsoption from server — the--max-requestsCLI option andSERVER_MAX_REQUESTSsetting have been removed (b48cdbafad33) - Unified server dispatch through
handler.handle(), consolidating the request pipeline for both HTTP/1.1 and HTTP/2 (e47efeb99332) - Fixed thread affinity in production request pipeline to ensure views run on the correct thread (6827fe551702)
Upgrade instructions
- Replace all
MyView.as_view()calls in URL patterns with justMyView. - Remove any
**kwargspassed toas_view()— set attributes on the class or override methods instead. - If you override
setup()in a view, move that logic toget(),post(), or another view method. - Remove any
--pidfileor--max-requestsflags from server invocations and theSERVER_PIDFILE/SERVER_MAX_REQUESTSsettings.
0.117.1 (2026-03-06)
What's changed
- Fixed 500 error handling to pass the actual exception to
ErrorViewinstead ofNone, allowing error views to access exception details (463177c8f0fa)
Upgrade instructions
- No changes required.
0.117.0 (2026-03-06)
What's changed
- Async view dispatch — views can now be
async. The server detects async view methods and awaits them directly on the worker's asyncio event loop instead of dispatching to the thread pool, freeing thread pool slots for sync views (b62c283ecd3d) AsyncStreamingResponse— new response class that streams from anasyncgenerator without occupying a thread pool slot. Available asfrom plain.http import AsyncStreamingResponse(b62c283ecd3d)ServerSentEventsView— new view class for Server-Sent Events. Subclass it and implement an asyncevents()generator that yieldsSentEventobjects. Handles SSE framing,Cache-Control, andX-Accel-Bufferingheaders automatically (b62c283ecd3d)
Upgrade instructions
- No changes required.
0.116.0 (2026-03-06)
What's changed
- HTTP/2 support — TLS connections now automatically negotiate HTTP/2 via ALPN. HTTP/2 requests are handled with full stream multiplexing using the
h2library, while HTTP/1.1 clients continue to work as before. Each HTTP/2 stream is dispatched to the thread pool independently, and idle connections time out after 5 minutes (c4d3a33671c6) - Middleware refactored to before/after phases —
HttpMiddlewareno longer uses the onion/wrapper model withprocess_request()andself.get_response(). Instead, middleware now implementsbefore_request(request) -> Response | None(return a response to short-circuit, orNoneto continue) andafter_response(request, response) -> Response(modify and return the response). The middleware pipeline runsbefore_requestforward through the chain, thenafter_responsein reverse. Middleware__init__no longer receivesget_response(9a1477ee8fa8) - Updated server architecture diagram and README to document HTTP/2 and the new middleware model (c4d3a33671c6, 9a1477ee8fa8)
Upgrade instructions
- Rename
process_request(self, request)tobefore_request(self, request)in custom middleware. The method should returnNoneto continue to the next middleware/view, or return aResponseto short-circuit. - Move any post-response logic (code after
self.get_response(request)) into a newafter_response(self, request, response)method that returns the response. - Remove
self.get_response(request)calls — the framework now handles calling the next middleware/view automatically. - Update
__init__signatures: middleware__init__no longer receivesget_response. Changedef __init__(self, get_response)todef __init__(self)and removesuper().__init__(get_response).
0.115.0 (2026-03-05)
What's changed
- Asyncio worker event loop — replaced the hand-rolled
selectorsevent loop andPollableMethodQueuepipe with Python'sasyncioas the worker's main loop. Connection acceptance, keepalive timeouts, and backpressure are now managed with native asyncio primitives (create_task,wait_for,Event,add_reader) while all request handling still runs synchronously in the thread pool viarun_in_executor(bc3f998f3fda) - Accept-loop crash detection — if a listener socket hits an unexpected error (e.g. EMFILE), the worker now detects it and shuts down for the arbiter to restart, instead of silently losing that listener (bc3f998f3fda)
- Cleaner graceful shutdown — uses
asyncio.wait()with timeout and task cancellation, and cancels accept loops before closing listener sockets to avoid EBADF errors (bc3f998f3fda)
Upgrade instructions
- No changes required.
0.114.1 (2026-03-04)
What's changed
- Fixed server error responses being malformed on Python 3.14 due to a
textwrap.dedent()behavior change that strips\ras whitespace, breaking the\r\n\r\nheader-body separator (6e61cf5e39b3)
Upgrade instructions
- No changes required.
0.114.0 (2026-03-04)
What's changed
- Lock-free thread worker event loop — replaced
RLockandfutures.wait()with a pipe-basedPollableMethodQueuethat defers worker thread completions back to the main thread, eliminating all lock contention in the connection handling hot path (d0ecd12bbe) - Unified event loop — the main loop now uses a single
poller.select()call for accepts, client data, and worker completions instead of splitting betweenpoller.select()andfutures.wait()(d0ecd12bbe) - Backpressure on accept — listener sockets are dynamically registered/unregistered from the poller when at connection capacity, preventing thread pool exhaustion under load (d0ecd12bbe)
- Slow client timeout — new connections now get a read timeout during request parsing so slow or stalled clients can't hold a thread pool slot indefinitely (d0ecd12bbe)
- Graceful shutdown improvement — shutdown now drains in-flight requests via the method queue instead of polling futures (d0ecd12bbe)
- Added
HTTPS_PROXY_HEADERupgrade warning to 0.113.0 changelog (b1d63fda04)
Upgrade instructions
- No changes required.
0.113.0 (2026-03-04)
What's changed
- Removed WSGI layer entirely — the server now creates
Requestobjects directly and writesResponseto sockets, eliminating the WSGI environ abstraction (163c31ba9f, 4a4fe406086a) Request.__init__now acceptsmethod,path,headers, and connection params directly instead of a WSGI environ dict (f25f430f54b4, 1c9ab9e67611)- Test client creates
Requestdirectly without WSGI environ (bed765f3ff77) - Centralized header normalization in
RequestHeadersclass, simplifyingRequestattributes (acec7dfd89be) - Removed
LimitedStream— body size limit is now enforced during reads (cb8ac54654f3) - Simplified
Responseby removing WSGI-erastart_responsemethod (7cea8c314449) - Server migrated from fork to spawn for process creation, with simplified process supervisor (a19c13255a4d)
- Flattened
Workerclasses and moved runtime setup to worker entry point (ce1114615d86) - Removed server-level scheme detection, unified on
HTTPS_PROXY_HEADERsetting (05eea6446830) - Added
Hostheader validation per RFC 9112 §3.2 (bd07db36aec2) - Structured access logging — server access log now uses structured context logging with configurable fields (72a905fbe1c3)
- Removed standard log format — only
keyvalueorjsonlog formats are supported (96ad632e6f28) - Replaced
Loggerclass with module-level functions and standard logging (24d9665818de) - Removed logging CLI options from server command (d00dc098b32d)
- Added
SERVER_*settings for server configuration (9fadf8bafec2) - Added per-response access log control and
ASSETS_LOG_304setting to suppress noisy asset 304s (4250db0ed02e) - Changed
--access-logfrom file path to boolean flag (6924211917f6) - Preflight badge rendered inline in HTML instead of JavaScript fetch (2894abfc5d98)
- Removed dead signal handlers (SIGUSR1, SIGUSR2, SIGWINCH, SIGHUP, SIGTTIN, SIGTTOU) (37f13c730b47)
- Deleted
Configdataclass — server params are passed directly (4989df5eb147) - Removed dead file-logging code from server (7f4f80fa0ff4)
Upgrade instructions
- WSGI removed — The WSGI layer (
plain.wsgi) has been completely removed. You can no longer use third-party WSGI servers (gunicorn, uvicorn, etc.) — useplain serverdirectly. If you had awsgi.pyentry point, remove it. Request()constructor changed —Request()now requiresmethodandpathkeyword arguments. Code that constructedRequestobjects directly (e.g., in tests) must be updated:Request(method="GET", path="/").request_startedsignal changed — The signal no longer sends anenvironkeyword argument. If you connected torequest_started, update your receiver to not expectenviron.WEB_CONCURRENCY→SERVER_WORKERS— The--workersCLI option now reads from theSERVER_WORKERSsetting (default:0for auto/CPU count).WEB_CONCURRENCYenv var is still read as a fallback in the default setting, but the canonical way is nowPLAIN_SERVER_WORKERSenv var orSERVER_WORKERSin settings.- New
SERVER_*settings — Server configuration has moved from CLI-only options to settings:SERVER_WORKERS,SERVER_THREADS,SERVER_TIMEOUT,SERVER_MAX_REQUESTS,SERVER_ACCESS_LOG,SERVER_ACCESS_LOG_FIELDS,SERVER_GRACEFUL_TIMEOUT,SERVER_SENDFILE. CLI flags still work as overrides. - Log format
standardremoved — Onlykeyvalueorjsonare supported. UpdateLOG_FORMATif you were usingstandard. - Server logging CLI options removed —
--log-level,--log-format, and--access-log-formathave been removed. Configure via settings or environment variables instead. --access-logis now a boolean — Use--access-log/--no-access-loginstead of passing a file path. Access logs always go to stdout.HTTPS_PROXY_HEADERnow required behind reverse proxies — The server no longer auto-detects HTTPS from the connection. If your app runs behind an SSL-terminating proxy (Heroku, AWS ALB, nginx, etc.) andHTTPS_REDIRECT_ENABLEDisTrue(the default), you must setHTTPS_PROXY_HEADERor you'll get an infinite 301 redirect loop. For example, on Heroku:HTTPS_PROXY_HEADER = "X-Forwarded-Proto: https"(or setPLAIN_HTTPS_PROXY_HEADER="X-Forwarded-Proto: https"as an env var).- Server process model changed from fork to spawn — This should be transparent, but if you relied on fork-inherited state in worker processes, it will no longer be available.
LimitedStreamremoved — Body size limits are now enforced automatically during reads.HEADER_MAPconfig removed — Underscore-containing headers are always dropped (the previous default behavior). Therefuseanddangerousoptions no longer exist.
0.112.1 (2026-03-03)
What's changed
settings.get_settings()now skips internal settings whose names start with_(7bd0064bdc)
Upgrade instructions
- No changes required.
0.112.0 (2026-02-28)
What's changed
- Removed
DEFAULT_CHARSETsetting — the charset is now alwaysutf-8, which was already the default. All references inQueryDict,ResponseBase, multipart parsing, and test utilities now use the hardcoded value directly (901e6b3c49)
Upgrade instructions
- If you were customizing
DEFAULT_CHARSETin your settings, remove it. UTF-8 is now always used.
0.111.0 (2026-02-26)
What's changed
- Added built-in
HEALTHCHECK_PATHsetting — when set, requests to this exact path return a200response before any middleware runs, avoiding ALLOWED_HOSTS rejection and HTTPS redirect loops from health checkers (2c25ccbadd) - Fixed test client to handle responses from middleware that bypass URL routing (e.g. healthcheck) — previously missing sessions or unresolvable paths would raise exceptions (bcd8913f02)
- Custom settings (prefixed with
APP_) now resolve type annotations properly, enabling environment variable parsing for custom settings (d9fff3223e) - Secret settings now show collection size hints (e.g.
{******** (3 items)}) instead of a flat********for dict, list, and tuple values (d9fff3223e) plain docs --searchnow supports--apito also search public API symbols (e3ef3f3d84)plain docs --sectionnow matches###subsections in addition to##sections (9db0491a3f)
Upgrade instructions
- No changes required.
0.110.1 (2026-02-26)
What's changed
- Added type annotations to all settings in
global_settings.pyso they can be set via environment variables — previously 11 settings likeHTTPS_REDIRECT_ENABLED,APPEND_SLASH, andFILE_UPLOAD_MAX_MEMORY_SIZEwere missing annotations and would error when set via env vars (37e8a58ca9b5)
Upgrade instructions
- No changes required.
0.110.0 (2026-02-26)
What's changed
- Environment variables now take highest precedence, overriding values set in
settings.py— previously explicit settings would win over env vars (0d40bcfcd539) - Moved SSL handshake from the main thread to the worker thread so handshake errors no longer crash the main loop, ported from gunicorn PR #3440 (6309ef82642e)
- Switched keepalive timeouts to use
time.monotonic()instead oftime.time()for correctness during clock adjustments (e7ddd1a31cfe) - Extracted
finish_body()method on the HTTP parser for explicit cleanup before returning keepalive connections to the poller (0cf51dd17c6f)
Upgrade instructions
- If you rely on
settings.pyvalues taking precedence overPLAIN_-prefixed environment variables, be aware that env vars now win. Remove any env vars that conflict with values you want to set in code.
0.109.0 (2026-02-26)
What's changed
- Added
--outlineflag toplain docsCLI to display section headings for quick navigation (153502ee90f5) - Added
--searchflag toplain docsCLI to find which modules and sections mention a term (153502ee90f5) - Enhanced
plain docs --listto show core modules alongside packages, with color-coded output (3f34b5405ea3) - Updated shell banner to show app name and version in a styled box instead of generic welcome message (a7b152d0baf8)
Upgrade instructions
- No changes required.
0.108.1 (2026-02-26)
What's changed
- Fixed
plain requestto uselocalhostas SERVER_NAME and default the Accept header totext/html, matching typical browser behavior (01731c5485cf) - Updated
plain-bugskill to create GitHub Issues viaghCLI instead of posting to the Plain API (ce7b95bd056d)
Upgrade instructions
- No changes required.
0.108.0 (2026-02-24)
What's changed
- Added absolute URL generation: new
absolute_url()andreverse_absolute()functions that prepend the scheme and domain using a newBASE_URLsetting (1e0d09f3ec70) - Added
reverse_absoluteandabsolute_urlas Jinja template globals for use in templates (1e0d09f3ec70) - Added
reverseas an explicit template global (previously only available asurl) (1e0d09f3ec70)
Upgrade instructions
- No changes required. To use absolute URLs, set
BASE_URLin your settings (e.g.BASE_URL = "https://example.com").
0.107.0 (2026-02-24)
What's changed
- Added
SettingOption— a custom Click option class that reads defaults from Plain settings, bridging CLI options to the settings system (cb5353b9d266) - Removed
SyncWorkerfrom the built-in HTTP server;ThreadWorkeris now the only worker type (c38ee93de5b4) - Changed
plain serverdefaults to--workers auto(one per CPU core) and--threads 4for better out-of-the-box concurrency (c38ee93de5b4)
Upgrade instructions
- If you relied on
SyncWorkeror single-threaded behavior, explicitly pass--threads 1toplain server. - If you pinned
--workers 1, note the default is nowauto. Pass--workers 1explicitly to keep the old behavior.
0.106.2 (2026-02-13)
What's changed
- Added
--versionflag to theplainCLI to display the installed version (e76fd7070302)
Upgrade instructions
- No changes required.
0.106.1 (2026-02-13)
What's changed
- Added
--sectionflag toplain docsfor loading a specific##section by name (e.g.,plain docs models --section querying) (f2ce0243e6ea) - Simplified
plain docsoutput by removing XML-style wrapper tags around documentation content (f2ce0243e6ea) - Added
/plain-bugskill for submitting bug reports to plainframework.com (a9efc4383233) - Slimmed agent rules to concise bullet-point reminders and moved detailed code examples into README docs (f5d2731ebda0)
- Added Forms section to templates README and View patterns section to views README (8c2189a896d2)
- Fixed agents exclusion in LLM docs to only exclude
.claude/content, allowingagents/README.mdto appear in docs output (f2ce0243e6ea)
Upgrade instructions
- No changes required.
0.106.0 (2026-02-12)
What's changed
- Added
plain checkcommand that runs core validation checks in sequence: custom commands, code linting, preflight, migration state, and tests (430268a12ae2) - Added assertion flags to
plain request:--status,--contains, and--not-containsfor automated response testing (6b66bfd05b9e) - Fixed
plain requestcrash when no user model exists (8ef8a3813bff) plain requestnow exits non-zero on server errors (5xx) and all failure paths (6b66bfd05b9e)- Updated agent rules with additional Django-to-Plain differences (model options, CSRF, forms, middleware) (9db8e0aa5d43)
Upgrade instructions
- No changes required.
0.105.0 (2026-02-05)
What's changed
plain agent installnow discovers and installs rules and skills fromplainx.*namespace packages in addition toplain.*packages (bd568db924f7)- Orphan cleanup during
plain agent installnow correctly handles bothplainandplainxprefixed items (bd568db924f7)
Upgrade instructions
- No changes required.
0.104.1 (2026-02-04)
What's changed
- Added
__all__exports to public modules for improved IDE autocompletion and explicit public API boundaries (e7164d3891b2, f26a63a5c941) - Removed
@internalcodedecorator from internal classes in favor of__all__exports (e7164d3891b2) - Renamed
plain docs --symbolsoption to--apifor clarity (e7164d3891b2)
Upgrade instructions
- If using
plain docs --symbols, update toplain docs --api.
0.104.0 (2026-02-04)
What's changed
- Refactored the assets manifest system with a clearer API:
AssetsManifestreplacesAssetsFingerprintsManifest, with explicit methodsadd_fingerprinted(),add_non_fingerprinted(),is_fingerprinted(), andresolve()(9cb84010b5fb) - Renamed
ASSETS_BASE_URLsetting toASSETS_CDN_URLfor clarity (9cb84010b5fb) - When
ASSETS_CDN_URLis configured,AssetsRouternow redirects compiled assets to the CDN: original paths use 302 (temporary), fingerprinted paths use 301 (permanent) with immutable caching (9cb84010b5fb) - The
is_immutable()check now uses the manifest to determine if a path is fingerprinted, rather than pattern-matching the filename (9cb84010b5fb)
Upgrade instructions
- Rename
ASSETS_BASE_URLtoASSETS_CDN_URLin your settings if you use a CDN for assets. - If you were importing from
plain.assets.fingerprints, update imports to useplain.assets.manifestinstead:AssetsFingerprintsManifest→AssetsManifestget_fingerprinted_url_path()→get_manifest().resolve()_get_file_fingerprint()→compute_fingerprint()
0.103.2 (2026-02-02)
What's changed
- Compiled assets now use deterministic gzip output by setting
mtime=0, ensuring consistent file hashes across builds (dc76e03879fc) - Agent rules now include a "Key Differences from Django" section to help Claude avoid common mistakes when working with Plain (02e11328dbf5)
Upgrade instructions
- No changes required.
0.103.1 (2026-01-30)
What's changed
load_dotenv()now sets each environment variable immediately as it is parsed, so command substitutions like$(echo $TOKEN)can reference variables defined earlier in the same.envfile (cecb71a016)
Upgrade instructions
- No changes required.
0.103.0 (2026-01-30)
What's changed
plain docsnow shows markdown documentation by default (previously required--source), with a new--symbolsflag to show only the symbolicated API surface (b71dab9d5d)plain docs --listnow shows all official Plain packages (installed and uninstalled) with descriptions and install status (9cba705d62)plain docsfor uninstalled packages now shows the install command and an online docs URL instead of a generic error (9cba705d62)- Removed the
plain agent contextcommand and theSessionStarthook setup — agent rules now provide context directly without needing a startup hook (88d9424643) plain agent installnow cleans up old SessionStart hooks from.claude/settings.json(88d9424643)
Upgrade instructions
- The
--sourceflag forplain docshas been removed. Use--symbolsinstead to see the symbolicated API surface. - The
--openflag forplain docshas been removed. - Run
plain agent installto clean up the old SessionStart hook from your.claude/settings.json.
0.102.0 (2026-01-28)
What's changed
- Refactored agent integration from skills-based to rules-based: packages now provide
agents/.claude/rules/files andagents/.claude/skills/directories instead ofskills/directories (512040ac51) - The
plain agent installcommand now copies both rules (.mdfiles) and skills to the project's.claude/directory, and cleans up orphanedplain*items (512040ac51) - Removed standalone skills (
plain-docs,plain-shell,plain-request) that are now provided as passive rules instead (512040ac51)
Upgrade instructions
- Run
plain agent installto update your.claude/directory with the new rules-based structure.
0.101.2 (2026-01-28)
What's changed
- When
load_dotenv()is called withoverride=False(the default), command substitution is now skipped for keys that already exist inos.environ. This prevents redundant command execution in child processes that re-load the.envfile after inheriting resolved values from the parent, avoiding multiple auth prompts with tools like the 1Password CLI (2f6ff93499) - The
_execute_commandhelper now usesstdout=subprocess.PIPEinstead ofcapture_output=True, allowing stderr/tty to pass through for interactive prompts (2f6ff93499) - Updated templates README examples to use
idinstead ofpk(837d345d23) - Added Settings section to README (803fee1ad5)
Upgrade instructions
- No changes required.
0.101.1 (2026-01-17)
What's changed
- Fixed a crash when running the development server with
--reloadwhen an app'sassetsdirectory doesn't exist (df33f93ece) - The
plain agent installcommand now preserves user-created skills (those without theplain-prefix) instead of removing them as orphans (bbc87498ed)
Upgrade instructions
- No changes required.
0.101.0 (2026-01-15)
What's changed
- The
plain servercommand now accepts--workers auto(orWEB_CONCURRENCY=auto) to automatically set worker count based on CPU count (02a1769948) - Response headers can now be set to
Noneto opt out of default headers;Nonevalues are filtered out at the WSGI layer rather than being deleted by middleware (cbf27e728d) - Removed unused
Responsemethods:serialize_headers,serialize, file-like interface stubs (write,flush,tell,readable,seekable,writable,writelines),textproperty, pickling support, andgetvalue(cbf27e728d)
Upgrade instructions
- No changes required
0.100.1 (2026-01-15)
What's changed
- The
plain agent installcommand now only sets up session hooks for Claude Code, not Codex, since thesettings.jsonhook format is Claude Code-specific (a41e08bcd2)
Upgrade instructions
- No changes required
0.100.0 (2026-01-15)
What's changed
- The
plain skillscommand has been renamed toplain agentwith new subcommands:plain agent install(installs skills and sets up hooks),plain agent skills(lists available skills), andplain agent context(outputs framework context) (fac8673436) - Added
SessionStarthook that automatically runsplain agent contextat the start of every Claude Code or Codex session, providing framework context without needing a separate skill (fac8673436) - The
plain-principlesskill has been removed - its content is now provided by theplain agent contextcommand via the SessionStart hook (fac8673436) - Added
--no-headersand--no-bodyflags toplain requestfor limiting output (fac8673436)
Upgrade instructions
- Replace
plain skills --installwithplain agent install - Replace
plain skills(without flags) withplain agent skills - Run
plain agent installto set up the new SessionStart hook in your project's.claude/or.codex/directory
0.99.0 (2026-01-15)
What's changed
- Added
plain.utils.dotenvmodule withload_dotenv()andparse_dotenv()functions for bash-compatible.envfile parsing, supporting variable expansion, command substitution, multiline values, and escape sequences (a9b2dc3e16)
Upgrade instructions
- No changes required
0.98.1 (2026-01-13)
What's changed
- Fixed
INSTALLED_PACKAGESnot being optional in user settings, restoring the default empty list behavior (820773c473)
Upgrade instructions
- No changes required
0.98.0 (2026-01-13)
What's changed
- The
plain skills --installcommand now removes orphaned skills from destination directories when skills are renamed or removed from packages (d51294ace1) - Added README documentation for
plain.skillswith available skills and installation instructions (7c90fc8595)
Upgrade instructions
- No changes required
0.97.0 (2026-01-13)
What's changed
- HTTP exceptions (
NotFoundError404,ForbiddenError403,BadRequestError400, andSuspiciousOperationError400variants) moved fromplain.exceptionstoplain.http.exceptionsand are now exported fromplain.http(b61f909e29)
Upgrade instructions
Update imports of HTTP exceptions from
plain.exceptionstoplain.http:# Before from plain.exceptions import NotFoundError404, ForbiddenError403, BadRequestError400 # After from plain.http import NotFoundError404, ForbiddenError403, BadRequestError400
0.96.0 (2026-01-13)
What's changed
- Response classes renamed for consistency:
ResponseRedirect→RedirectResponse,ResponseNotModified→NotModifiedResponse,ResponseNotAllowed→NotAllowedResponse(fad5bf28b0) - Redundant response classes removed:
ResponseNotFound,ResponseForbidden,ResponseBadRequest,ResponseGone,ResponseServerError- useResponse(status_code=X)instead (fad5bf28b0) - HTTP exceptions renamed to include status code suffix:
Http404→NotFoundError404,PermissionDenied→ForbiddenError403,BadRequest→BadRequestError400,SuspiciousOperation→SuspiciousOperationError400(5a1f020f52) - Added
Secret[T]type annotation for masking sensitive settings likeSECRET_KEYin CLI output (8713dc08b0) - Added
ENV_SETTINGS_PREFIXESsetting to configure which environment variable prefixes are checked for settings (defaults to["PLAIN_"]) (8713dc08b0) - New
plain settings listandplain settings getCLI commands for viewing settings with their sources (8713dc08b0) - Added preflight check for unused environment variables matching configured prefixes (8713dc08b0)
- Renamed
request.metatorequest.environfor clarity (786b95bef8) - Added
request.query_stringandrequest.content_lengthproperties (786b95bef8, 76dfd477d2) - Renamed X-Forwarded settings:
USE_X_FORWARDED_HOST→HTTP_X_FORWARDED_HOST,USE_X_FORWARDED_PORT→HTTP_X_FORWARDED_PORT,USE_X_FORWARDED_FOR→HTTP_X_FORWARDED_FOR(22f241a55c) - Changed
HTTPS_PROXY_HEADERfrom a tuple to a string format (e.g.,"X-Forwarded-Proto: https") (7ac2a431b6)
Upgrade instructions
Replace Response class imports and usages:
ResponseRedirect→RedirectResponseResponseNotModified→NotModifiedResponseResponseNotAllowed→NotAllowedResponseResponseNotFound→Response(status_code=404)ResponseForbidden→Response(status_code=403)ResponseBadRequest→Response(status_code=400)ResponseGone→Response(status_code=410)ResponseServerError→Response(status_code=500)
Replace exception imports and usages:
Http404→NotFoundError404PermissionDenied→ForbiddenError403BadRequest→BadRequestError400SuspiciousOperation→SuspiciousOperationError400SuspiciousMultipartForm→SuspiciousMultipartFormError400SuspiciousFileOperation→SuspiciousFileOperationError400TooManyFieldsSent→TooManyFieldsSentError400TooManyFilesSent→TooManyFilesSentError400RequestDataTooBig→RequestDataTooBigError400
Replace
request.metawithrequest.environRename X-Forwarded settings in your configuration:
USE_X_FORWARDED_HOST→HTTP_X_FORWARDED_HOSTUSE_X_FORWARDED_PORT→HTTP_X_FORWARDED_PORTUSE_X_FORWARDED_FOR→HTTP_X_FORWARDED_FOR
Update
HTTPS_PROXY_HEADERfrom tuple format to string format:# Before HTTPS_PROXY_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # After HTTPS_PROXY_HEADER = "X-Forwarded-Proto: https"Replace
plain setting <name>command withplain settings get <name>
0.95.0 (2025-12-22)
What's changed
- Improved thread worker server shutdown behavior with
cancel_futures=Truefor faster and cleaner process termination (72d0620)
Upgrade instructions
- No changes required
0.94.0 (2025-12-12)
What's changed
FormFieldMissingErrorexceptions are now automatically converted to HTTP 400 Bad Request responses with a warning log instead of causing a 500 error (b38f6e5)
Upgrade instructions
- No changes required
0.93.1 (2025-12-09)
What's changed
- Added type annotation for
request.unique_idattribute to improve IDE support and type checking (23af501)
Upgrade instructions
- No changes required
0.93.0 (2025-12-04)
What's changed
- Improved type annotations across forms, HTTP handling, logging, and other core modules for better IDE support and type checking (ac1eeb0)
- Internal refactor of
TimestampSignerto use composition instead of inheritance fromSigner, maintaining the same public API (ac1eeb0)
Upgrade instructions
- No changes required
0.92.0 (2025-12-01)
What's changed
- Added
request.client_ipproperty to get the client's IP address, with support forX-Forwarded-Forheader when behind a trusted proxy (cb0bc5d) - Added
USE_X_FORWARDED_FORsetting to enable reading client IP fromX-Forwarded-Forheader (cb0bc5d) - Improved
print_eventCLI output styling with dimmed text for less visual noise (b09edfd)
Upgrade instructions
- No changes required
0.91.0 (2025-11-24)
What's changed
- Request body parsing refactored: the
request.dataattribute has been replaced withrequest.json_dataandrequest.form_datafor explicit content-type handling (90332a9) QueryDictnow has proper type annotations forget(),pop(),getlist(), and__getitem__()methods that reflect string return types (90332a9)- Forms now automatically select between
json_dataandform_databased on request content-type (90332a9) - View mixins
ObjectTemplateViewMixinremoved in favor of class inheritance for better typing -UpdateViewandDeleteViewnow inherit fromDetailView(569afd6) AppLoggercontext logging now uses acontextdict parameter instead of**kwargsfor better type checking (581b406)- Removed erroneous
AuthViewMixinexport fromplain.views(334bbb6)
Upgrade instructions
Replace
request.datawith the appropriate method:- For JSON requests: use
request.json_data(returns a dict, raisesBadRequestfor invalid JSON) - For form data: use
request.form_data(returns aQueryDict)
- For JSON requests: use
Update
app_loggercalls that pass context as kwargs to use thecontextparameter:# Before app_logger.info("Message", user_id=123, action="login") # After app_logger.info("Message", context={"user_id": 123, "action": "login"})
0.90.0 (2025-11-20)
What's changed
- Improved type annotations in
timezone.py:is_aware()andis_naive()now accept bothdatetimeandtimeobjects for more flexible type checking (a43145e) - Enhanced type annotations in view classes:
convert_value_to_response()and handler result variables now use more explicit type hints for better IDE support (dc4454e) - Fixed type errors in forms and server workers: URL field now handles bytes properly, and worker wait_fds has explicit type annotation (fc98d66)
Upgrade instructions
- No changes required
0.89.0 (2025-11-14)
What's changed
- Improved type annotations in view classes:
url_args,url_kwargs, and various template/form context dictionaries now have more specific type hints for better IDE support and type checking (83bcb95)
Upgrade instructions
- No changes required
0.88.0 (2025-11-13)
What's changed
- The
plain.formsmodule now uses explicit imports instead of wildcard imports, improving IDE autocomplete and type checking support (eff36f3)
Upgrade instructions
- No changes required
0.87.0 (2025-11-12)
What's changed
- Internal classes now use abstract base classes with
@abstractmethoddecorators instead of raisingNotImplementedError, improving type checking and IDE support (91b329a, 81b5f88, d2e2423, 61e7b5a) - Updated to latest version of
tytype checker and fixed type errors and warnings throughout the codebase (f4dbcef)
Upgrade instructions
- No changes required
0.86.2 (2025-11-11)
What's changed
- CLI color output is now enabled in CI environments by checking the
CIenvironment variable, matching the behavior of modern tools like uv (a1500f15ed)
Upgrade instructions
- No changes required
0.86.1 (2025-11-10)
What's changed
- The
plain preflightcommand now outputs to stderr only when using--format json, keeping stdout clean for JSON parsing while avoiding success messages appearing in error logs for text format (72ebee7729) - CLI color handling now follows the CLICOLOR standard with proper priority:
NO_COLOR>CLICOLOR_FORCE/FORCE_COLOR>CLICOLOR>isatty(c7fea406c5)
Upgrade instructions
- No changes required
0.86.0 (2025-11-10)
What's changed
- Log output is now split by severity level: INFO and below go to stdout, WARNING and above go to stderr for proper cloud platform log classification (52403b15ba)
- Added
LOG_STREAMsetting to customize log output behavior with options:"split"(default),"stdout", or"stderr"(52403b15ba) - Log configuration documentation expanded with detailed guidance on output streams and environment variable settings (52403b15ba)
Upgrade instructions
- No changes required (default behavior splits logs to stdout/stderr automatically, but this can be customized via
PLAIN_LOG_STREAMenvironment variable if needed)
0.85.0 (2025-11-03)
What's changed
- CLI help output now organizes commands into "Common Commands", "Core Commands", and "Package Commands" sections for better discoverability (73d3a48)
- CLI help output has been customized with improved formatting and shortcut indicators showing which commands are shortcuts (e.g.,
migrate → models migrate) (db882e6) - CSRF exception messages now include more detailed context about what was rejected and why (e.g., port mismatches, host mismatches) (9a8e09c)
- The
plain agent mdcommand now saves a combinedAGENTS.mdfile to.plain/by default when usingplain dev, making it easier to provide context to coding agents (786b7a0) - CLI help text styling has been refined with dimmed descriptions and usage prefixes for improved readability (d7f7053)
Upgrade instructions
- No changes required
0.84.1 (2025-10-31)
What's changed
- Added
license = "BSD-3-Clause"to package metadata inpyproject.toml(8477355)
Upgrade instructions
- No changes required
0.84.0 (2025-10-29)
What's changed
- The
DEFAULT_RESPONSE_HEADERSsetting now supports format string placeholders (e.g.,{request.csp_nonce}) for dynamic header values instead of requiring a callable function (5199383128) - Views can now set headers to
Noneto explicitly remove default response headers (5199383128) - Added comprehensive documentation for customizing default response headers including override, remove, and extend patterns (5199383128)
Upgrade instructions
If you have
DEFAULT_RESPONSE_HEADERSconfigured as a callable function, convert it to a dictionary with format string placeholders:# Before: def DEFAULT_RESPONSE_HEADERS(request): nonce = request.csp_nonce return { "Content-Security-Policy": f"script-src 'self' 'nonce-{nonce}'", } # After: DEFAULT_RESPONSE_HEADERS = { "Content-Security-Policy": "script-src 'self' 'nonce-{request.csp_nonce}'", }If you were overriding default headers to empty strings (
"") to remove them, change those toNoneinstead
0.83.0 (2025-10-29)
What's changed
- Added comprehensive Content Security Policy (CSP) documentation explaining how to use nonces with inline scripts and styles (784f3dd972)
- The
json_scriptutility function now accepts an optionalnonceparameter for CSP-compliant inline JSON scripts (784f3dd972)
Upgrade instructions
- Any
|json_scriptusages need to make sure the second argument is a nonce, not a custom encoder (which is now third)
0.82.0 (2025-10-29)
What's changed
- The
DEFAULT_RESPONSE_HEADERSsetting can now be a callable that accepts a request argument, enabling dynamic header generation per request (cb92905834) - Added
request.csp_noncecached property for generating Content Security Policy nonces (75071dcc70) - Simplified the preflight command by moving
plain preflight checkback toplain preflight(40c2c4560e)
Upgrade instructions
- If you use
plain preflight check, update toplain preflight(thechecksubcommand has been removed for simplicity) - If you use
plain preflight check --deploy, update toplain preflight --deploy
0.81.0 (2025-10-22)
What's changed
- Removed support for category-specific error template fallbacks like
4xx.htmland5xx.html(9513f7c4fa)
Upgrade instructions
- If you have
4xx.htmlor5xx.htmlerror templates, rename them to specific status code templates (e.g.,404.html,500.html) or remove them if you prefer the plain HTTP response fallback
0.80.0 (2025-10-22)
What's changed
- CSRF failures now raise
SuspiciousOperation(HTTP 400) instead ofPermissionDenied(HTTP 403) (ad146bde3e) - Error templates can now use category-specific fallbacks like
4xx.htmlor5xx.htmlinstead of the genericerror.html(716cfa3cfc) - Updated error template documentation with best practices for self-contained
500.htmltemplates (55cea3b522)
Upgrade instructions
- If you have a
templates/error.htmltemplate, instead create specific error templates for each status code you want to customize (e.g.,400.html,403.html,404.html,500.html). You can also create category-specific templates like4xx.htmlor5xx.htmlfor broader coverage.
0.79.0 (2025-10-22)
What's changed
- Response objects now have an
exceptionattribute that stores the exception that caused 5xx errors (0a243ba89c) - Middleware classes now use an abstract base class
HttpMiddlewarewith aprocess_request()method (b960eed6c6) - CSRF middleware now raises
PermissionDeniedinstead of rendering a customCsrfFailureView(d4b93e59b3) - The
HTTP_ERROR_VIEWSsetting has been removed (7a4e3a31f4) - Standalone
plain-changelogandplain-upgradeexecutables have been removed in favor of the built-in commands (07c3a4c540) - Standalone
plain-buildexecutable has been removed (99301ea797) - Removed automatic logging of all HTTP 400+ status codes for cleaner logs (c2769d7281)
Upgrade instructions
If you have custom middleware, inherit from
HttpMiddlewareand rename your__call__()method toprocess_request():# Before: class MyMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): response = self.get_response(request) return response # After: from plain.http import HttpMiddleware class MyMiddleware(HttpMiddleware): def process_request(self, request): response = self.get_response(request) return responseRemove any custom
HTTP_ERROR_VIEWSsetting from your configuration - error views are now controlled entirely by exception handlersIf you were calling
plain-changelogorplain-upgradeas standalone commands, useplain changelogorplain upgradeinsteadIf you were calling
plain-buildas a standalone command, useplain buildinstead
0.78.2 (2025-10-20)
What's changed
- Updated package metadata to use
[dependency-groups]instead of[tool.uv]for development dependencies, following PEP 735 standard (1b43a3a272)
Upgrade instructions
- No changes required
0.78.1 (2025-10-17)
What's changed
- Fixed job worker logging by using
getLoggerinstead of directly instantiatingLoggerfor the plain logger (dd675666b9)
Upgrade instructions
- No changes required
0.78.0 (2025-10-17)
What's changed
- Chores have been refactored to use abstract base classes instead of decorated functions (c4466d3c60)
- Added
SHELL_IMPORTsetting to customize what gets automatically imported inplain shell(9055f59c08) - Views that return
Nonenow raiseHttp404instead of returningResponseNotFound(5bb60016eb) - The
plain chores listcommand output formatting now matches theplain jobs listformat (4b6881a49e)
Upgrade instructions
Update any chores from decorated functions to class-based chores:
# Before: @register_chore("group") def chore_name(): """Description""" return "Done!" # After: from plain.chores import Chore, register_chore @register_chore class ChoreName(Chore): """Description""" def run(self): return "Done!"Import
Chorebase class fromplain.choreswhen creating new chores
0.77.0 (2025-10-13)
What's changed
- The
plain server --reloadnow useswatchfilesfor improved cross-platform file watching (92e95c5032) - Server reloader now watches
.env*files for changes and triggers automatic reload (92e95c5032) - HTML template additions and deletions now trigger automatic server reload when using
--reload(f2f31c288b) - Internal server worker type renamed from "gthread" to "thread" for clarity (6470748e91)
Upgrade instructions
- No changes required
0.76.0 (2025-10-12)
What's changed
- Added new
plain servercommand with built-in WSGI server (vendored gunicorn) (f9dc2867c7) - The
plain servercommand supportsWEB_CONCURRENCYenvironment variable for worker processes (0c3e8c6f32) - Simplified server startup logging to use a single consolidated log line (b1405b71f0)
- Removed
gunicornas an external dependency - server functionality is now built into plain core (cb6c2f484d) - Internal server environment variables renamed from
GUNICORN_*toPLAIN_SERVER_*(745c073123) - Removed unused server features including hooks, syslog, proxy protocol, user/group dropping, and config file loading (be0f82d92b, 10c206875b, ecf327014c, fb5a10f50b)
Upgrade instructions
- Replace any direct usage of
gunicornwith the newplain servercommand (ex.gunicorn plain.wsgi:app --workers 4becomesplain server --workers 4) - Update any deployment scripts or Procfiles that use
gunicornto useplain serverinstead - Remove
gunicornfrom your project dependencies if you added it separately (it's now built into plain) - For Heroku deployments, the
$PORTis not automatically detected - update your Procfile toweb: plain server --bind 0.0.0.0:$PORT - If you were using gunicorn configuration files, migrate the settings to
plain servercommand-line options (runplain server --helpto see available options)
0.75.0 (2025-10-10)
What's changed
- Documentation references updated from
plain-workertoplain-jobsfollowing the package rename (24219856e0)
Upgrade instructions
- No changes required
0.74.0 (2025-10-08)
What's changed
- The
plain agent requestcommand now displays request ID in the response output (4a20cfa3fc) - Request headers are now included in OpenTelemetry tracing baggage for improved observability (08a3376d06)
Upgrade instructions
- No changes required
0.73.0 (2025-10-07)
What's changed
- Internal preflight result handling updated to use
model_optionsinstead of_metafor model label retrieval (73ba469)
Upgrade instructions
- No changes required
0.72.2 (2025-10-06)
What's changed
- Improved type annotations for test client responses with new
ClientResponsewrapper class (369353f9d6) - Enhanced internal type checking for WSGI handler and request/response types (50463b00c3)
Upgrade instructions
- No changes required
0.72.1 (2025-10-02)
What's changed
- Fixed documentation examples to use the correct view attribute names (
self.userinstead ofself.request.user) (f6278d9)
Upgrade instructions
- No changes required
0.72.0 (2025-10-02)
What's changed
- Request attributes
userandsessionare no longer set directly on the request object (154ee10) - Test client now uses
plain.auth.requests.get_request_user()to retrieve user for response object when available (154ee10) - Removed
plain.auth.middleware.AuthenticationMiddlewarefrom default middleware configuration (154ee10)
Upgrade instructions
- No changes required
0.71.0 (2025-09-30)
What's changed
- Renamed
HttpRequesttoRequestthroughout the codebase for consistency and simplicity (cd46ff20) - Renamed
HttpHeaderstoRequestHeadersfor naming consistency (cd46ff20) - Renamed settings:
APP_NAME→NAME,APP_VERSION→VERSION,APP_LOG_LEVEL→LOG_LEVEL,APP_LOG_FORMAT→LOG_FORMAT,PLAIN_LOG_LEVEL→FRAMEWORK_LOG_LEVEL(4c5f2166) - Added
request.get_preferred_type()method to select the most preferred media type from Accept header (b105ba4d) - Moved helper functions in
http/request.pyto be static methods ofQueryDict(0e1b0133)
Upgrade instructions
- Replace all imports and usage of
HttpRequestwithRequest - Replace all imports and usage of
HttpHeaderswithRequestHeaders - Update any custom settings that reference
APP_NAMEtoNAME,APP_VERSIONtoVERSION,APP_LOG_LEVELtoLOG_LEVEL,APP_LOG_FORMATtoLOG_FORMAT, andPLAIN_LOG_LEVELtoFRAMEWORK_LOG_LEVEL - Configuring these settings via the
PLAIN_prefixed environment variable will need to be updated accordingly
0.70.0 (2025-09-30)
What's changed
- Added comprehensive type annotations throughout the codebase for improved IDE support and type checking (365414c)
- The
Assetclass inplain.assets.findersis now a module-level public class instead of being defined insideiter_assets()(6321765)
Upgrade instructions
- No changes required
0.69.0 (2025-09-29)
What's changed
- Model-related exceptions (
FieldDoesNotExist,FieldError,ObjectDoesNotExist,MultipleObjectsReturned,EmptyResultSet,FullResultSet) moved fromplain.exceptionstoplain.models.exceptions(1c02564) - Added
plain devalias prompt that suggests addingpas a shell alias for convenience (d913b44)
Upgrade instructions
- Replace imports of
FieldDoesNotExist,FieldError,ObjectDoesNotExist,MultipleObjectsReturned,EmptyResultSet, orFullResultSetfromplain.exceptionstoplain.models.exceptions - If you're using
ObjectDoesNotExistin views, update your import fromplain.exceptions.ObjectDoesNotExisttoplain.models.exceptions.ObjectDoesNotExist
0.68.1 (2025-09-25)
What's changed
- Preflight checks are now sorted by name for consistent ordering (cb8e160)
Upgrade instructions
- No changes required
0.68.0 (2025-09-25)
What's changed
- Major refactor of the preflight check system with new CLI commands and improved output (b0b610d461)
- Preflight checks now use descriptive IDs instead of numeric codes (cd96c97b25)
- Unified preflight error messages and hints into a single
fixfield (c7cde12149) - Added
plain-upgradeas a standalone command for upgrading Plain packages (42f2eed80c)
Upgrade instructions
- Update any uses of the
plain preflightcommand toplain preflight check, and remove the--databaseand--fail-leveloptions which no longer exist - Custom preflight checks should be class based, extending
PreflightCheckand implementing therun()method - Preflight checks need to be registered with a custom name (ex.
@register_check("app.my_custom_check")) and optionally withdeploy=Trueif it should run in only in deploy mode - Preflight results should use
PreflightResult(optionally withwarning=True) instead ofpreflight.Warningorpreflight.Error - Preflight result IDs should be descriptive strings (e.g.,
models.lazy_reference_resolution_failed) instead of numeric codes PREFLIGHT_SILENCED_CHECKSsetting has been replaced withPREFLIGHT_SILENCED_RESULTSwhich should contain a list of result IDs to silence.PREFLIGHT_SILENCED_CHECKSnow silences entire checks by name.
0.67.0 (2025-09-22)
What's changed
ALLOWED_HOSTSnow defaults to[](empty list) which allows all hosts, making it easier for development setups (d3cb7712b9)- Empty
ALLOWED_HOSTSin production now triggers a preflight error instead of a warning to ensure proper security configuration (d3cb7712b9)
Upgrade instructions
- No changes required
0.66.0 (2025-09-22)
What's changed
- Host validation moved to dedicated middleware and
ALLOWED_HOSTSsetting is now required (6a4b7be) - Changed
request.get_port()method torequest.portcached property (544f3e1) - Removed internal
request._get_full_path()method (50cdb58)
Upgrade instructions
- Add
ALLOWED_HOSTSsetting to your configuration if not already present (required for host validation) - Replace any usage of
request.get_host()withrequest.host - Replace any usage of
request.get_port()withrequest.port
0.65.1 (2025-09-22)
What's changed
- Fixed DisallowedHost exception handling in request span attributes to prevent telemetry errors (bcc0005)
- Removed cached property optimization for scheme/host to improve request processing reliability (3a52690)
Upgrade instructions
- No changes required
0.65.0 (2025-09-22)
What's changed
- Added CIDR notation support to
ALLOWED_HOSTSfor IP address range validation (c485d21)
Upgrade instructions
- No changes required
0.64.0 (2025-09-19)
What's changed
- Added
plain-buildcommand as a standalone executable (4b39ca4) - Removed
constant_time_compareutility function in favor ofhmac.compare_digest(55f3f55) - CLI now forces colors in CI environments (GitHub Actions, GitLab CI, etc.) for better output visibility (56f7d2b)
Upgrade instructions
- Replace any usage of
plain.utils.crypto.constant_time_comparewithhmac.compare_digestorsecrets.compare_digest
0.63.0 (2025-09-12)
What's changed
- Model manager attribute renamed from
objectstoquerythroughout codebase (037a239) - Simplified HTTPS redirect middleware by removing
HTTPS_REDIRECT_EXEMPT_PATHSandHTTPS_REDIRECT_HOSTsettings (d264cd3) - Database backups are now created automatically during migrations when
DEBUG=Trueunless explicitly disabled (c802307)
Upgrade instructions
- Remove any
HTTPS_REDIRECT_EXEMPT_PATHSandHTTPS_REDIRECT_HOSTsettings from your configuration - the HTTPS redirect middleware now performs a blanket redirect. For advanced redirect logic, write custom middleware.
0.62.1 (2025-09-09)
What's changed
- Added clarification about
app_logger.kvremoval to 0.62.0 changelog (106636f)
Upgrade instructions
- No changes required
0.62.0 (2025-09-09)
What's changed
- Complete rewrite of logging settings and AppLogger with improved formatters and debug capabilities (ea7c953)
- Added
app_logger.debug_mode()context manager to temporarily change log level (f535459) - Minimum Python version updated to 3.13 (d86e307)
- Removed
app_logger.kvin favor of context kwargs (ea7c953)
Upgrade instructions
- Make sure you are using Python 3.13 or higher
- Replace any
app_logger.kv.info("message", key=value)calls withapp_logger.info("message", key=value)or appropriate log level
0.61.0 (2025-09-03)
What's changed
- Added new
plain agentcommand with subcommands for coding agents includingdocs,md, andrequest(df3edbf) - Added
-coption toplain shellto execute commands and exit, similar topython -c(5e67f0b) - The
plain docs --llmfunctionality has been moved toplain agent docscommand (df3edbf) - Removed the
plain helpcommand in favor of standardplain --help(df3edbf)
Upgrade instructions
- Replace
plain docs --llmusage withplain agent docscommand - Use
plain --helpinstead ofplain helpcommand
0.60.0 (2025-08-27)
What's changed
- Added new
APP_VERSIONsetting that defaults to the project version frompyproject.toml(57fb948d46) - Updated
get_app_name_from_pyproject()toget_app_info_from_pyproject()to return both name and version (57fb948d46)
Upgrade instructions
- No changes required
0.59.0 (2025-08-22)
What's changed
- Added new
APP_NAMEsetting that defaults to the project name frompyproject.toml(1a4d60e) - Template views now validate that
get_template_names()returns a list instead of a string (428a64f) - Object views now use cached properties for
.objectand.objectsto improve performance (bd0507a) - Improved
plain upgradecommand to suggest using subagents when there are more than 3 package updates (497c30d)
Upgrade instructions
- In object views,
self.load_object()is no longer necessary asself.objectis now a cached property.
0.58.0 (2025-08-19)
What's changed
- Complete rewrite of CSRF protection using modern Sec-Fetch-Site headers and origin validation (955150800c)
- Replaced CSRF view mixin with path-based exemptions using
CSRF_EXEMPT_PATHSsetting (2a50a9154e) - Renamed
HTTPS_REDIRECT_EXEMPTtoHTTPS_REDIRECT_EXEMPT_PATHSwith leading slash requirement (b53d3bb7a7) - Agent commands now print prompts directly when running in Claude Code or Codex Sandbox environments (6eaed8ae3b)
Upgrade instructions
- Remove any usage of
CsrfExemptViewMixinandrequest.csrf_exemptand add exempt paths to theCSRF_EXEMPT_PATHSsetting instead (ex.CSRF_EXEMPT_PATHS = [r"^/api/", r"/webhooks/.*"]-- but consider first whether the view still needs CSRF exemption under the new implementation) - Replace
HTTPS_REDIRECT_EXEMPTwithHTTPS_REDIRECT_EXEMPT_PATHSand ensure patterns include leading slash (ex.[r"^/health$", r"/api/internal/.*"]) - Remove all CSRF cookie and token related settings - the new implementation doesn't use cookies or tokens (ex.
{{ csrf_input }}and{{ csrf_token }})
0.57.0 (2025-08-15)
What's changed
- The
ResponsePermanentRedirectclass has been removed; useResponseRedirectwithstatus_code=301instead (d5735ea) - The
RedirectView.permanentattribute has been replaced withstatus_codefor more flexible redirect status codes (12dda16) - Updated
RedirectViewinitialization parameters:url_namereplacespattern_name,preserve_query_paramsreplacesquery_string, and removed 410 Gone functionality (3b9ca71)
Upgrade instructions
- Replace
ResponsePermanentRedirectimports withResponseRedirectand passstatus_code=301to the constructor - Update
RedirectViewsubclasses to usestatus_code=301instead ofpermanent=True - Replace
pattern_namewithurl_namein RedirectView usage - Replace
query_string=Truewithpreserve_query_params=Truein RedirectView usage
0.56.1 (2025-07-30)
What's changed
- Improved
plain installcommand instructions to be more explicit about completing code modifications (83292225db)
Upgrade instructions
- No changes required
0.56.0 (2025-07-25)
What's changed
- Added
plain installcommand to install Plain packages with agent-assisted setup (bf1873e) - Added
--printoption to agent commands (plain installandplain upgrade) to print prompts without running the agent (9721331) - The
plain docscommand now automatically converts hyphens to dots in package names (e.g.,plain-models→plain.models) (1e3edc1) - Moved
plain-upgradefunctionality into plain core, eliminating the need for a separate package (473f9bb)
Upgrade instructions
- No changes required
0.55.0 (2025-07-22)
What's changed
- Updated URL pattern documentation examples to use
idinstead ofpkin URL kwargs (b656ee6) - Updated views documentation examples to use
idinstead ofpkfor DetailView, UpdateView, and DeleteView (b656ee6)
Upgrade instructions
- Update your URL patterns from
<int:pk>to<int:id>in your URLconf - Update view code that accesses
self.url_kwargs["pk"]to useself.url_kwargs["id"]instead - Replace any QuerySet filters using
pkwithid(e.g.,Model.query.get(pk=1)becomesModel.query.get(id=1))
0.54.1 (2025-07-20)
What's changed
- Fixed OpenTelemetry route naming to include leading slash for consistency with HTTP paths (9d77268)
Upgrade instructions
- No changes required
0.54.0 (2025-07-18)
What's changed
- Added OpenTelemetry instrumentation for HTTP requests, views, and template rendering (b0224d0418)
- Added
plain-observerpackage reference to plain README (f29ff4dafe)
Upgrade instructions
- No changes required
0.53.0 (2025-07-18)
What's changed
- Added a
pluralizefilter for Jinja templates to handle singular/plural forms (4cef9829ed) - Added
get_signed_cookie()method toHttpRequestfor retrieving and verifying signed cookies (f8796c8786) - Improved CLI error handling by using
click.UsageErrorinstead of manual error printing (88f06c5184) - Simplified preflight check success message (adffc06152)
Upgrade instructions
- No changes required
0.52.2 (2025-06-27)
What's changed
- Improved documentation for the assets subsystem: the
AssetsRouterreference in the Assets README now links directly to the source code for quicker navigation (65437e9)
Upgrade instructions
- No changes required
0.52.1 (2025-06-27)
What's changed
- Fixed
plain helpoutput on newer versions of Click by switching fromMultiCommandtoGroupwhen determining sub-commands (9482e42)
Upgrade instructions
- No changes required
0.52.0 (2025-06-26)
What's changed
- Added
plain-changelogas a standalone executable so you can view changelogs without importing the full framework (e4e7324) - Removed the runtime dependency on the
packaginglibrary by replacing it with an internal version-comparison helper (e4e7324) - Improved the error message when a package changelog cannot be found, now showing the path that was looked up (f3c82bb)
- Fixed an f-string issue that broke
plain.debug.ddon Python 3.11 (ed24276)
Upgrade instructions
- No changes required
0.51.0 (2025-06-24)
What's changed
- New
plain changelogCLI sub-command to quickly view a package’s changelog from the terminal. Supports--from/--toflags to limit the version range (50f0de7).
Upgrade instructions
- No changes required
0.50.0 (2025-06-23)
What's changed
- The URL inspection command has moved; run
plain urls listinstead of the oldplain urlscommand (6146fcb) plain preflightgains a simpler--databaseflag that enables database checks for your default database. The previous behaviour that accepted one or more database aliases has been removed (d346d81)- Settings overhaul: use a single
DATABASEsetting instead ofDATABASES/DATABASE_ROUTERS(d346d81)
Upgrade instructions
Update any scripts or documentation that call
plain urls …:- Replace
plain urls --flatwithplain urls list --flat
- Replace
If you invoke preflight checks in CI or locally:
- Replace
plain preflight --database <alias>(or multiple aliases) with the new boolean flag:plain preflight --database
- Replace
In
settings.pymigrate to the new database configuration:# Before DATABASES = { "default": { "ENGINE": "plain.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", } } # After DATABASE = { "ENGINE": "plain.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", }Remove any
DATABASESandDATABASE_ROUTERSsettings – they are no longer read.