Changelog
All notable changes to tanGO are recorded here. The format follows Keep a Changelog, and what each version number promises is described in Versioning and compatibility.
Unreleased
0.6.0 - 2026-10-21
Added
-
Admin operational dashboard (opt-in).
admin.WithDashboard(admin.DashboardConfig{Status, Activity})turns/admin/into a page with database and migration status and recent request and job activity; without it, or with neither source set, the landing page is unchanged. The status comes from a function you supply (build it fromtango.Status), and the activity from anyobservability.Snapshotter. A failing or panicking source marks only its own card "unavailable" and logs the cause; the error text never reaches the page. New inobservability:SnapshotterandSnapshot(a read side that is separate fromRecorder),NewWindow(an opt-in in-memory collector: the last 60 minutes in one-minute buckets, HTTP requests by status class and latency, job runs by outcome, nothing per route or job, reset on restart, labelled "this instance only"), andMulti(feed several recorders at once). It is a starting point for "is this instance healthy right now", not a metrics store; keep an external monitoring system for history, alerts and several instances. -
MySQL 8.4 support.
mysql://user:pass@host:3306/dbinTANGO_DB_DSNselects the newdb.MySQLdialect, withgithub.com/go-sql-driver/mysqlas the driver. Migrations,Store, the admin,tango newproject --dialect=mysqlandtestdb(a fresh database per test;TANGO_TEST_DSN=mysql://…,testdb.MySQLOnly,testdb.NotMySQL) all work on it, and CI runs the suite on MySQL 8.4. Tables are InnoDB with theutf8mb4_bincollation; timestamps areDATETIME(6)in UTC; connections useREAD COMMITTEDunless the DSN sets another isolation level, and the default pool recycles a connection after five minutes. MariaDB is not supported. Differences to know: an unboundedstringthat isuniqueorindexed isVARCHAR(255)(a longer value returns a*db.ValueTooLongError),LIKEis case-sensitive, and a migration that fails part-way cannot roll back (the error names every applied step and writes no tracking row). See the setup guide. A project with ashell/main.gofrom before this release must copy the newdatabaseLabelfrom the shell guide before using MySQL, ortango shellprints the DSN's password. -
Admin dark mode. Every admin page now follows the browser's light or dark preference, and signed-in staff can pick Light, Dark or System from the sidebar footer; the choice is kept in a
tango_admin_themecookie scoped to/admin/. The light theme is nearly unchanged: input, checkbox and secondary-button borders are a shade darker, and primary and danger buttons darken on hover, so they meet WCAG contrast. Colours are now semantic tokens (bg-surface,text-ink,border-line, the status family, and so on) generated from one palette table, with contrast tests over its pairs in both themes. Widgets that hard-code light colours stay light, so switch them to the token classes in the admin guide; the Markdown preview stays a white canvas in both themes. There is nothing to configure.
Changed
-
AccessLoggeris free when its logger discards Info events. It used to wrap the response and build the event's attributes for every request and only then let the logger drop it (3 allocations a request). It now asks the logger first and calls the next handler directly. A logger that logs Info behaves as before; the level is read on every request, so aslog.LevelVarstill takes effect at once. -
Dependencies.
go.modnow requires chi 5.5.0 (was 5.3.2),golang.org/x/crypto0.58.0,x/sys0.49.0,x/term0.47.0 andmodernc.org/sqlite1.60.1, and addsgithub.com/go-sql-driver/mysql1.10.1 for the MySQL dialect. A project that depends on tanGO is raised to at least these versions the next time it updates; rungo mod tidy.
Documentation
- Choosing
MaxOpen. The configuration guide now explains how the size of the database pool shapes tail latency, with measured figures, and suggests starting near the database's CPU count.
0.5.0 - 2026-10-17
Added
- OpenAPI documents.
openapi.New(openapi.Options{...})inInstalledAppsserves an OpenAPI 3.1 document of every included route at/openapi.json(opt-in: nothing is served otherwise).openapi.Devis the same app served only whenTANGO_OPENAPI=1, so a deployed process exposes nothing unless it sets the variable. Each route is listed with its method, path, path parameters, name and a tag from its prefix.tango.Doc(tango.RouteDoc{...})adds a summary, a request body, query parameters, responses and a bearer requirement, with schemas reflected from Go types (encoding/jsontags, nested and recursive types,varchar=nasmaxLength); a type can name its schema withSchemaName().tango checkfails and names a route whose documented types cannot be described. TheaccountsJSON endpoints document themselves; the admin's pages and the cookie-flow pages are hidden from the document.RouteRegistry.Describedandtango.DescribedRouteexpose the route table to tools.tango newprojectnow leaves a comment inproject/project.gosaying where to add it. See OpenAPI documentation. tango openapi [-o file]prints or writes the document (the new-tango-openapiapp-side flag, handled byDispatchFlags;DumpOpenAPI,OpenAPIProviderandErrNoOpenAPI). The output is the same bytes on every run, and a failed run leaves an existing file untouched.- Swagger UI.
openapi.Options{UI: swaggerui.New()}adds a browsable page at/docs/from Swagger UI 5.17.14 embedded in the binary (Apache-2.0; no network request). It is the separate packageopenapi/swaggeruiand adds about 1.7 MB to a binary that imports it.
Changed
-
Database connections are reused under load.
database/sqlkeeps only 2 idle connections, so a server handling more than 2 requests at once closed and reopened a database connection on almost every request (measured: about 10,900 PostgreSQL sessions opened in one 15-second run of single-pet reads).ServeContextnow raises the idle limit to 32 unless your app bounded the pool withSetMaxOpenConns.tango.WithDBPool(tango.DBPool{...})sets the limits explicitly;tango.WithDBPool(tango.DBPool{})restores the previous behavior. The number of open connections is unchanged (unlimited). -
Foreign keys are not indexed unless you say so (guidance). A
fk=column is not indexed by itself, so filtering or joining on it scans the table (measured:liston a 10,000-row table ran at about 1,300 requests a second without the index and about 3,200 with it). Writetango:"fk=Category,index"on foreign keys you filter or join on. Indexing foreign keys by default is planned for a later minor release; this entry is the notice.
Fixed
- The admin no longer comes up silently empty. The admin builds its pages when it is installed, so a model registered by an app listed after
admin.Newnever appeared in it.tango check(and-check) now fails and names those models, thetango newprojectscaffold says to add apps aboveadmin.New, andtango newapprepeats it when the project installs the admin. Moveadmin.Newafter your apps.
0.4.0 - 2026-10-10
Added
-
JSON fields.
tango:"json"on astringfield declares it holds JSON text (model.FieldMeta.JSON).Store.CreateandStore.Updaterefuse text that is not valid JSON with a*db.InvalidJSONErrorthat matchesdb.ErrInvalidJSONand never contains the submitted text; the empty string is unset,nullis valid, and the stored text is exactly what was given. It is ordinaryTEXTon SQLite and PostgreSQL, so adding the tag to an existing field generates no migration and does not scan existing rows; reads are not checked.jsoncannot be combined withvarchar=n,text,pk,fk,uniqueorindex. See ADR 0059. -
JSON fields in the admin. A
tango:"json"field getsadmin.JSON()by default (Edit and Formatted tabs) with noOptions.Widgetsentry; an explicit entry still wins, and read-only and changelist rendering are unchanged. A save the store refuses for invalid JSON, including through a non-JSON widget such asadmin.Textarea(), re-renders with a 422 and a translatable field error (admin.error.invalid_json, "Not valid JSON.") beside the field, keeping the text as typed. See admin registration. -
Admin refinement.
admin.JSON()edits a string field as JSON (syntax checked on save, the submitted text stored exactly, a browser-side Formatted tab).admin.Markdown(...)edits Markdown in a monospace textarea with a character count and, when you passadmin.WithMarkdownPreview, a sandboxed Preview tab from your own renderer: tanGO ships no parser or sanitizer, and you decide how HTML is sanitized (ADR 0058).Options.Filtersadds boolean and foreign-key filters to the changelist (they combine with search and ordering; an over-large foreign-key filter still applies a primary key given in the URL).Options.Displaywithadmin.DisplayFuncandadmin.Displayrenders computed changelist cells, such as a file's size and an authorized download link, without admin knowing about storage;admin.ByteSizeandadmin.ShortHashhelp.admin.FieldErrorlets a widget report an error beside its own field. Forms now renderHelpTextand mark invalid fields witharia-invalidandaria-describedby; the changelist gets a labelled search box and filters, a caption and column scopes.Options.Labelsnow also names a field's changelist column when set. See admin registration and ADRs 0057 and 0058. -
Bounded strings.
tango:"varchar=n"declares astringfield of at mostncharacters (1 to 10,485,760), andtango:"text"states an unbounded one explicitly; a barestringstays unbounded text and generates no migration.model.FieldMeta.MaxLengthcarries the limit and a malformed tag failsRegisterwithmodel.ErrInvalidFieldTag.Store.CreateandStore.Updaterefuse a longer value before any SQL with a*db.ValueTooLongError(db.ErrValueTooLong), counting runes the way PostgreSQL counts characters; SQLite does not enforce the length and raw SQL is not checked.tango makemigrationswrites aVARCHAR(n)column (Column.Length, additive, with noCHECK), generates the widening changes (varchar(n)totextor a largervarchar) and the narrowing ones (texttovarchar(n)or a smallervarchar), and a narrowing checks the existing data in Go first, failing with the number of rows and one offending key instead of truncating anything. A migration of narrowings is reversible; a widening makes its migration irreversible. The admin rendersmaxlengthand shows a translatable per-field message (admin.error.too_long) for a refused save, andadmin.FieldContextgainsMaxLength. Built-in models are not retagged. See models and tags and changing a field's type. -
Accounts for a single-page app.
accounts.WithJSONmounts six bearer-only JSON endpoints under/accounts/api/(register, login, password reset request and confirm, verify, resend verification) for a separate client, sharing the HTML flow's rules, limiters and outbox. The host supplies the token side throughaccounts.JSONAuth(Issue,Authenticate) andAuthGrant, soaccountsimports no token package; JSON mode never reads or sets a cookie and has no logout.JSONConfigalso takes the emailed link templatesVerifyURLandResetURL({token}once; prefer a fragment), anOnRegisterhook that runs inside the account's transaction with aRegistration(aFieldErroror*db.ValueTooLongErroranswers422), andResolveIdentifierso login can take a username. Requests are strict (415,413,400 invalid_body), errors are{"error", "code", "fields"?}, and every response isno-store. Newdb.Store.InTxruns a callback in one transaction (commit, rollback on error, rollback and re-panic on panic; nested calls join). See the accounts guide, the newexamples/spa-accountsand ADRs 0050 and 0051. -
Accounts answers invalid JSON as a field error. In the JSON accounts API, a
*db.InvalidJSONErrorfrom a registration hook'stx.Create(alone or joined witherrors.Join) answers422 invalid_field, keyed by the Go field name with the messagemust be valid JSON, instead of a generic500; the document is never echoed. See the accounts guide. -
File uploads and storage. The new
storagepackage is an explicit object store for uploaded files, used from ordinary Views:Putstreams a file in, always sniffs its content type, enforces a required size cap and an optional allowed-types list, and returns a generated opaque key with the size and SHA-256;Stat,Open(a byte range) andDeletecomplete theStore. There is noFileField, no file model, no listing and no signed URLs: the host keeps the key and metadata in its own model and authorizes every access.storage.Uploadreads one multipart file with bounded memory, andstorage.Servedelivers an authorized object with ranges, conditional requests, attachment by default andnosniff. Backends:storage/local(atomic,os.Root-contained, for development and one host),storage/s3(Amazon S3, Cloudflare R2 and other S3-compatible providers, in its own Go module so no cloud SDK reaches yourgo.mod), andstoragetestwith an in-memoryStoreand the conformance suite everyStorepasses. See the uploads guide, the newexamples/uploadsand ADRs 0052 to 0054. Native Google Cloud Storage is a later release. -
Caching. The new
cachepackage is a small, explicit cache for recomputable reads: a bytes-onlycache.Store(Get,Setwith a TTL of up to 30 days,Delete) where a miss is not an error, one portable key rule,cache.Prefixfor namespaces and bulk invalidation by version, typed JSON helpers (GetJSON,SetJSON,ErrCorrupt), and the fail-openFetchJSONread-through that reports cache failures to anOnErrorhook and never fails the request because the cache is down.cache/localis a bounded in-process store (entry limit, optional byte budget, least-recently-used eviction);cache/redis(Redis and Valkey) andcache/memcacheare their own Go modules, so no client library reaches yourgo.mod.cachetest.Runis the conformance suite every backend passes. There is no response, fragment or ORM caching, no automatic invalidation, and nothing logs. See the caching guide, the newexamples/cacheand ADRs 0055 and 0056.
Changed
-
An admin changelist
?page=beyond the last page now shows the last page, so the rows and the pager always describe the same page (before, it showed an empty list under a pager that named the last page). -
The admin foreign key select now starts with a "— None —" choice (translation key
admin.fk.none), and an unset foreign key shows an em dash in the changelist instead of0. Saving "None" stores the same unset value (NULL) as before. -
POST /admin/logout/now requires the session's CSRF token when the request carries a session cookie, so a cross-site form cannot sign an admin out. A missing or wrong token gets 403 and leaves the session in place. A request without a session cookie still redirects to the login page. -
The admin sidebar footer no longer says "Signed in over Basic Auth" (admin has used session logins since 0.3). It now shows "Signed in as " with a Log out button that posts to
/admin/logout/with the session's CSRF token. The translation keyadmin.layout.signed_inis replaced byadmin.layout.signed_in_asandadmin.layout.logout. -
/accounts/login/now always spends one password comparison, even for an unknown or inactive account, so its timing no longer says whether an account exists. Nothing else about the HTML flow changes.
0.3.1 - 2026-10-09
Fixed
db.Querywith anOffsetand noLimitfailed on SQLite with a SQL syntax error (SQLite refusesOFFSETwithoutLIMIT).Listnow skips the rows and returns the rest on both dialects. It also madetango shell'sList(model, map[string]any{"offset": n})fail the same way.
0.3.0 - 2026-10-09
Added
tango shell: a Go prompt on your project's models and database, with nothing served. It is a Yaegi interpreter (pinned at v0.16.1) run by the project's ownshell/main.gothrough the newshellpackage, so the server never links it. HelpersModels,Describe,Get,List,Count,Create,UpdateandDeletework on any registered model addressed asapp.Model, with plain-map queries and rows;-c EXPRand piped input make it scriptable;--readonlyrefuses writes;Options.Helpersadds your own functions asproject.Name. At a terminal it has line editing, per-project history under your cache directory (TANGO_SHELL_HISTORY=offdisables it), and Ctrl-C that clears the line, or ends a running evaluation with status 130. It says what Go the interpreter cannot run (min,max,clear, some generics, ranging over a function or an integer) and keeps the session through errors and panics. It runs your code with your project's credentials; it is not a sandbox. See the shell guide, ADR 0048. Addsgithub.com/traefik/yaegito the root module; only a binary that importsshelllinks it.admin.WithTrustedProxiesandaccounts.WithTrustedProxies: behind a reverse proxy, the failed-attempt limiters count the client address your proxy saw (the rightmostX-Forwarded-Forentry outside the networks you name) instead of the proxy's own, so users no longer share one bucket. Opt-in: without it nothing is read fromX-Forwarded-Forand behaviour is unchanged, and only requests from the named networks may supply a forwarded address. The limiter also no longer keeps an entry for an address that has no recent failure. See behind a reverse proxy.- Running more than one instance: what is shared across instances (sessions, accounts, tokens), what is per process (the login limiter,
ratelimit,realtimerooms, theaccountsmail outbox and cooldown) and the workarounds, with ADR 0047 recording that shared state is not built yet and what would reopen it. No API changes. - A
tango tuiguide, with screens checked against the code by a test, and the promise about the dashboard in versioning and compatibility: the command, its terminal requirement, confirmation before database changes and delegation to the supported operations are covered; layout, wording and key bindings are not.
Changed
tango newprojectwrites two more directories.project/project.goholdsfunc Config(store *db.Store) tango.Config: the installed apps, middleware and ordinary configuration thatmain.goused to build inline, so the server and the shell register the same apps from one list.main.goopens the database, builds the store and callsproject.Config(store).shell/main.gois the programtango shellruns.tango newappnow says to install a new app inproject/project.gowhen that file exists. Existing projects keep working as servers; to get a shell they follow the documented upgrade, a diff tested against the scaffold as it was before (ADR 0048).- The tutorial and the
examples/boardproject list their apps inproject/project.go; the board's token service and live feed are built insideproject.Config, which stops the process with their error message when they cannot start. tango admin createandtango admin resetpassworduseTANGO_ADMIN_PASSWORDwhen it is set and non-empty, with no prompt. It was documented but never read. The value is never printed; the command names the variable instead.tango migratesays what it did:applied 2 migrations: admin/0001_auto_…, blog/0002_auto_…, orno pending migrations, instead of always printingmigrations applied. A project gets the new wording when it is built against this version; the rollback line is unchanged.tango newprojectends with the real first-run order (tango makemigrations,tango migrate,tango admin create <username>), and the generated project printsadmin: http://localhost:8000/admin/afterlistening on(not with--no-admin). Existing projects are unchanged.tango tui:- the greyed-out "Shell" item is gone;
- "Apply pending migrations" and "Roll back the latest migration" grey out with a reason when status shows they cannot run (nothing to apply, nothing to roll back, or project status incomplete);
- the confirmation prompts say what they will do.
Security
ratelimit.RemoteIPKey(trustedProxies...)could be made to give a client the key it chose. It took the leftmostX-Forwarded-Forentry, which the client writes when the proxy appends to the header (nginx's default), and it did not check that the value was an IP address, so a client behind a trusted proxy could rotate its key to evade a limit or flood the limiter with new keys. It also fell back toX-Real-IP. It now readsX-Forwarded-Foronly from a trusted peer, parses every entry as an IP address, walks the list from the right skipping trusted proxies, and takes the first other address, which is the one your nearest trusted proxy saw.X-Real-IPis ignored, and a valid address is returned in canonical form. Unusable data falls back to the peer's address. The failed-login limiters ofadminandaccounts(admin.WithTrustedProxies,accounts.WithTrustedProxies) use the same resolver. This changes behaviour under the compatibility promise's security exception.- Upgrade: nothing changes without trusted proxies. With them, check that your proxy adds the address it sees to
X-Forwarded-For(appending and replacing both work; passing the header through untouched does not). If you relied onX-Real-IP, make the proxy put the client address inX-Forwarded-For. Keys for IPv6 clients are now lower-case and compressed, and IPv4-mapped IPv6 addresses are keyed as IPv4, so a limit that was counted under the old spelling restarts once.
- Upgrade: nothing changes without trusted proxies. With them, check that your proxy adds the address it sees to
Fixed
tango admin createandresetpasswordno longer show the password as you type it on a terminal. Piped input is read as before. This addsgolang.org/x/termto the root module (ADR 0046).- Ctrl-C leaves
tango tuieven while a confirmation is showing; it used to be ignored until you answered. - Projects created by
tango newprojectnow shut down gracefully: the generatedmain.gocallstango.ServeContextundersignal.NotifyContextfor Ctrl-C andSIGTERM, so in-flight requests finish and registeredLifecyclecomponents stop. It calledtango.Serve, which never reacts to a signal. Existing applications are unchanged; swap inServeContextas the application lifecycle guide shows. tango tuino longer ends the session when an apply or rollback fails. It showsLast action failed: …with the exit code and the command's last error line, refreshes the status, and returns to the menu; if the refresh also fails, both errors show.- Stopping the server started from
tango tuiwith Ctrl-C orSIGTERMis a clean stop: tango waits for the server, printsserver stoppedand exits 0. It used to die with the signal. A server that fails, or exits with no signal, keeps its exit code. tango tuiexplains a status failure (runtango check; an oldermain.gomay needtango.DispatchFlags) before the raw error, which is kept.
0.2.0 - 2026-10-08
Changed
-
Projects created by
tango newprojectopt in to everything new in this release:LoadConfigFromEnv(tango.WithPortFromEnv()), which replaces the hardcoded:8000and honoursTANGO_ADDRandPORT;RequestID,RecovererandAccessLogger;- a global
MaxBodySize(1 << 20); MiddlewareScopeAll.
Existing applications keep their behaviour unless they opt in.
-
accountsmodels gainAccount.EmailVerifiedAtand a newAccountTokenmodel, for password reset and email verification. Every app that installsaccountsmust runtango makemigrationsand apply the result with-migrate, whether or not it enables mail: the change only adds a column and a table, so it needs no--renameor--allow-drop. Existing accounts read as unverified; nothing is gated on it unless an app usesaccounts.RequireVerified. -
The
routeattribute is now the route's registered pattern in every log and metric for a request, trailing slash included (/items/{id}/), under both middleware scopes. Thetango.http.access,tango.http.panicandtango.http.request_id_generation_failedlogs used to drop a trailing slash (/items/{id}), whilectx.Logger(),tango.http.view_errorand the HTTP metrics did not, so the same request reported two routes. Dashboards or alerts that match these three events on a trimmedrouteneed the registered spelling. The attribute key is unchanged.
Added
- A
mailpackage for outgoing email:mail.Sender,Messagewith in-memory attachments (sent asmultipart/mixed, 10 MiB of raw attachments by default), and three senders.NewSMTPSender/SMTPSenderFromEnv(TANGO_SMTP_URL) always encrypt:smtp://requires STARTTLS,smtps://is TLS from the start, andsmtp+insecure://is plaintext to a loopback host only, with an explicit port. Errors never contain credentials, recipients or messages.WriterSenderis for development, andmail/mailtestis for tests. See ADR 0044 and the mail guide. accounts.WithMailturns on password reset (/accounts/password-reset/) and email verification (/accounts/verify/), andaccounts.RequireVerifiedguards a View on a verified email. Reset never reveals whether an address has an account, links are single-use, hashed, tied to the address they were sent to and built on a configuredBaseURL, and GET never uses a link. Emails go through a bounded in-memory outbox, which logstango.accounts.mail_failedandtango.accounts.mail_dropped. WithoutWithMail,accountsbehaves as before. See ADR 0045.Config.NotFoundandConfig.MethodNotAllowedViews replace the router's404and405responses (tanGO setsAllowfor a405). TheAllowheader lists every method routed at that path, including a non-standard one the app registered. UnderMiddlewareScopeAllthey run inside global middleware and are observed; underMiddlewareScopeRoutesthey run outside it.Config.MiddlewareScope: withtango.MiddlewareScopeAll, global middleware wraps the whole router, so Unmatched requests (the router's404and405) are logged, counted, recovered and body-limited, reported with route"(unmatched)". Global middleware then runs before routing, so it can't read route parameters or rewrite which route answers. The default staysMiddlewareScopeRoutes, today's behaviour, and may becomeMiddlewareScopeAllonly after a minor release of notice; set the scope explicitly to keep yours. See ADR 0043.tango.LoadConfigFromEnv(tango.WithPortFromEnv())falls back to thePORTvariable hosting platforms set: the address isTANGO_ADDR, else":"+PORT, else:8000. Without the option,LoadConfigFromEnvis unchanged.tango.MaxBodySize(n)middleware caps request bodies: an oversized body gets413with{"error":"request body too large"}. When several limits apply, the most restrictive wins.
Security
ServeContext(andServe) now drop a client that takes more than 10 seconds to send a request's headers, so a slow-header (Slowloris-style) client can't hold connections open indefinitely. This changes default behaviour under the compatibility promise's security exception.tango.WithReadHeaderTimeout(d)sets another bound, andWithReadHeaderTimeout(0)restores the old, unbounded behaviour.
Fixed
tango newappnow prints how to install the new app: the import path read fromgo.modand theInstalledAppsentry to add tomain.go. A new app used to be created silently and answer 404 until wired in.- The compatibility page promised a JSON body for every
403,405and429from/accounts/*. It now promises only the JSONaccountssends, for a rejected CSRF token (403) and rate limiting (429, withRetry-After). Closed registration's403is an HTML page, and the router's405has no body. A test pins that contract.
0.1.0 - 2026-10-08
Changed
- Breaking: the database is chosen by
TANGO_DB_DSN's scheme alone, andTANGO_DB_DIALECTis retired: setting it is now an error.tango.LoadDBDSNFromEnvandtango.LoadDBDialectFromEnvare replaced bytango.LoadDBConfigFromEnv, which returns adb.DSNcarrying the dialect, driver name and connection string together.- Upgrade: put the scheme in the DSN (
TANGO_DB_DSN=postgres://…, orsqlite://app.db; unset still meanssqlite://app.db), dropTANGO_DB_DIALECT, and replace the two loader calls withdsn, err := tango.LoadDBConfigFromEnv()followed bysql.Open(dsn.Driver, dsn.Source)anddsn.Dialect. Projects generated bytango newprojectfrom this release on already do this.
- Upgrade: put the scheme in the DSN (
- Breaking:
tango makemigrationsrefuses to write a migration that drops a table or column unless each drop is authorised with--allow-drop app.Modelor--allow-drop app.Model.Field, and refuses model changes no migration can express (a narrowing type change, a change of primary key or of a foreign key's target). A refused run writes nothing and prints the commands to choose between.- Upgrade: if a run is refused, declare a rename with
--renameor authorise the drop with--allow-drop, as the error message shows.
- Upgrade: if a run is refused, declare a rename with
- Breaking:
migration.Diffandmigration.DiffModelsreturn an error alongside the migrations. These are part of the CLI-internal migration surface, which applications don't normally call.- Upgrade: handle the second return value.
- SQLite writers wait up to five seconds for a held lock instead of failing at once with
database is locked. - Breaking:
realtime/websocket.Viewgained a variadic...ViewOptionparameter. Calls compile unchanged, but code that storesViewin a variable of its old function type no longer compiles.- Upgrade: add
...websocket.ViewOptionto that variable's function type, or callViewdirectly.
- Upgrade: add
Added
db.ParseDSN, which parses a scheme-qualified DSN into adb.DSN.- Renaming a field or a model with
tango makemigrations --rename, which keeps the column or table and its rows. - Widening type changes (
integertorealortext,realtotext,booleantointegerortext) through the newAlterColumnTypestep. - The public
testdbpackage: an app's own tests get a fresh database for the run's Test dialect, chosen byTANGO_TEST_DSN. examples/board, the tutorial's app, tested end to end on SQLite and PostgreSQL.realtime/websocket.Viewtakes options:WithOriginPatternsandWithInsecureSkipVerifyconfigure the upgrade's origin check.
Fixed
- Dropping a column on SQLite keeps everything else about the table: rows, indexes, unique constraints, foreign keys in both directions, defaults,
NOT NULLand the primary key. NULLreads as the zero value, and an unset foreign key is written asNULL.- Cookies are marked
Securebehind a TLS-terminating proxy that setsX-Forwarded-Proto. - Application sessions work on PostgreSQL.
- Times read back in UTC on every dialect.
- Generated migrations order their steps, and migrations across apps, by foreign key, so they apply on PostgreSQL.
- Migration files generated by v0.0.1 and v0.0.2 apply and roll back on PostgreSQL even when a table references one whose name sorts after it: the runner now creates referenced tables first and drops referencing tables first within a migration.
- Cascade delete handles circular foreign key references.
- Every SQL identifier tanGO generates is quoted, so a model named after a reserved word (
User,Order) works on both dialects. - Creating a row with an explicit ID on PostgreSQL advances the ID sequence.
tango newprojectkeeps its standard-library imports in one sorted group.
0.0.2 - 2026-09-23
The v0.0.2 tag on GitHub was later moved to a commit adding realtime/websocket.View options; the Go module proxy, and so every go get, serves the original commit (4fd6667), which this section and its comparison link describe. Those options are listed under 0.1.0.
Added
- The
observabilitypackage, with aRecorderinterface for metrics and stable event and metric names. - Structured HTTP logging: the
RequestID,RecovererandAccessLoggermiddleware, and HTTP duration metrics. - Background jobs: registration, scheduling and structured failure reporting.
ServeContext, with lifecycle-aware graceful shutdown, andLifecycleregistration onRegistry.- The
ratelimitpackage: a token-bucketLimiter,KeyFunc,RemoteIPKeyandMiddleware. realtimeHub operations report to the sharedRecorder.
Changed
Servedelegates toServeContext.
Fixed
- Every
Lifecycle.Stopcall shares one stop-phase budget. RemoteIPKeyhandles nil trusted CIDRs.ratelimit.Limiter.Takerejects costs a bucket can never hold.Limitercleans up idle buckets without overflowing.
0.0.1 - 2026-09-21
Added
- The first public release: the
tangopackage (apps, models, routing with reverse lookup, Views, middleware, checks),db.Storeon SQLite and PostgreSQL, schema migrations generated bytango makemigrations, and thetangoCLI (newproject,newapp,run,check,migrate,tui,admin). - The
adminapp: a default theme, session-cookie login with CSRF protection and rate-limited attempts, staff and superuser tiers, foreign key selects, and best-effort widgets and branding. - Many-to-one relationships, cascade delete, and bounded
Where/Any/Countquery filtering. - Reusable apps that ship their own migrations and static assets.
- The
authpackage for application sessions, theaccountsapp for registration, login and logout, andauth/jwtfor HS256 bearer tokens. - The
i18npackage. - The
realtimepackage, with single-owner rooms and generation-safe timers, and itsrealtime/websocketadapter.