Project
Changelog
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. Before v1.0.0, minor versions may contain breaking changes.
#Unreleased
#1.16.0 - 2026-11-12
Secure by default, and typed SQL. This release turns on protections that apps had to remember before, so upgrading takes a few steps:
- Run
go tool tern fix access. Routes with server code now say who may use them, and the build stops at one that doesn't; the command marks each such routekit.Public, as it was. Then tighten the ones that should be:kit.SignedIn, orkit.Permission("…"). - If a route uses
kit.SignedIn, give the app the check:acc.Guard()withaccounts, orkit.WithSignedIn. Withkit.Permission, alsopolicy.Guard(). - Pages now get a Content-Security-Policy. If they load scripts, frames or connections from other origins, such as analytics, allow them with
kit.WithCSPSources;tern devshows what's blocked. storage.ServeneedsServeOptions.Signer, orPublic: truefor files anyone may see.- Behind a reverse proxy, name it with
kit.WithTrustedProxies, so the new rate limit tells clients apart; the app warns until you do.
#Added
tern doctor(docs/deployment.md) checks a project before a deploy: Go and Node, the Go module's and npm package's versions, code generation, native rendering's fallbacks, the migrations, the target's environment against.env.example, HTTPS, security headers and uncommitted changes. It exits 1 when a check fails.- Typed SQL (ADR 0028, docs/sql.md): queries in
queries/*.sql, named with-- name: LinksOf :many, become typed Go functions inqueries/queries.gen.go. Parameters are named (:user_id) and always bound; their types and the rows' come from the schema the migrations build, checked in a scratch SQLite database at build time, so a renamed column failstern checkat every query that used it. The same queries run on SQLite and PostgreSQL. The SaaS starter template keeps its projects this way. - Migration checks:
tern migrate checkandtern doctorfind migrations that fail or break the app during a deploy: aNOT NULLcolumn without a default, dropped or renamed tables and columns, changes to every row, and type changes that lock a PostgreSQL table. A SQLite app's migrations run up, down and up again in a scratch database.-- safe: <why>acknowledges a statement;migrate.Checkruns the checks from Go. settings: an app's settings read from the environment into a struct byenvtags, with defaults, required settings and minimum lengths; every missing or invalid one is reported at once when the app starts.settings.Secretnever shows its value infmt,slogor JSON.sealed: encrypted columns for personal data,sealed.Stringandsealed.Bytes, with AES-256-GCM, rotating keys,sealed.Blindhashes to find rows by a value, andsealed.AllowPlainfor columns being encrypted. Typed SQL can use them.safehttp: an HTTP client for addresses users give, which only connects to public addresses, checked on the address dialed, and limits redirects, ports, time and response size. Theimagesplugin and OIDC's client metadata use it.- Uploads without photo metadata:
storage.Upload.Saveremoves EXIF (with GPS coordinates), XMP, IPTC and comments from JPEG and PNG images, leaving the pixels as they are;ReceiveOptions.KeepMetadatakeeps them. - Operations in the admin panel (docs/admin.md):
acc.UsersAdmin()andacc.AuditAdmin()for accounts and their audit log,queue.Admin()for background jobs with Retry, andadmin.Errors(reporter)for error reports grouped by cause. - Actions on records in the admin panel:
admin.Actionandadmin.DangerActionadd buttons to each record's page that run Go code. acc.SignOutUserends every session of an account.- Shelf's admin panel has its users, audit log and errors.
- The Tern vs SvelteKit benchmark in CI compares each run on
mainwith the last one, Tern's binary size included, and warns when Tern is more than 10% slower.
#Changed
- Secure by default (ADR 0027, docs/security.md). These change what a running app sends; each has an option to restore the old behaviour.
- Every response carries the security headers
plugins/secureused to add:X-Frame-Options: DENY, a strictReferrer-Policy, aPermissions-Policythat turns off the camera, microphone and location, andCross-Origin-Opener-Policy: same-origin.plugins/securenow changes them;kit.WithoutSecurityHeaders()drops them. - Every page is sent a strict
Content-Security-Policy: only the app's own scripts run, and inline scripts with the request's nonce, which Tern now also puts on inline scripts in<svelte:head>. Svelte's own inline event handler (this.__e=event) is allowed by its hash, and apps with Go components in the browser get'wasm-unsafe-eval'.kit.WithCSPSourcesallows more origins, such as an analytics script;kit.WithoutContentSecurityPolicy()sends none. Undertern dev, the overlay shows what the policy blocked and the line that allows it. - Routes are closed by default. A route with server code declares who may use it,
var Access = kit.Public,kit.SignedInorkit.Permission("…"), and code generation stops at one that doesn't.SignedInandPermissionapply to the directories below too; Tern checks them before the loads, actions and endpoints run, redirecting pages to sign in and answering 401 from endpoints, and never serves such a route from the page cache.acc.Guard()andpolicy.Guard()(orkit.WithSignedInandkit.WithPermissions) are the checks. To upgrade, rungo tool tern fix access, which marks each route without a rulekit.Public, as it was, then tighten the ones that should be. - Form actions and other state-changing requests are limited to 120 a minute per client, in bursts of 60; a client over the limit gets 429 with
Retry-After. Loopback clients aren't limited, and the app warns when requests come through a proxykit.WithTrustedProxiesdoesn't name. Addingplugins/ratelimitreplaces the limit;kit.WithoutRateLimit()removes it.
- Every response carries the security headers
tern routesshows who may use each route, and the MCP server'snew_routetool asks, defaulting to signed-in users.- The examples and starter templates declare who may use each route; their signed-in areas use
kit.SignedIn. storage.ServeneedsServeOptions.SignerorPublic: true: files are private unless the app says otherwise.- The
imagesplugin fetches originals only from public addresses by default; pass anHTTPClientfor originals on a private network. tern doctorchecks for options that turn protections off, and says what each costs, instead of checking forplugins/secure.tern doctorlooks for known vulnerabilities: govulncheck, built in, on the Go modules and Go version, failing for those the app's code reaches, andnpm auditorpnpm auditon the npm packages, warning for high and critical advisories.-offlineskips them. Tern's own CI runs both.- Smaller binaries with native rendering. The compiled renderer's function descriptors are static data instead of code that runs at startup, modules' state starts out with a loop instead of one assignment per variable, and a binary without the JavaScript engine leaves out the server bundle it no longer needs. The dashboard example's binary goes from 38.9 to 37.4 MB, and starts faster.
#Fixed
- The SaaS starter template's default app name, used for passkeys and as the email sender, was the literal text
{{.Name}}. - Native rendering: a
vardeclared in a loop or block at a module's top level no longer crashes the compiler;\p{ID_Start}and\p{ID_Continue}work in regular expressions; and a sticky or global regular expression after non-ASCII text starts wherelastIndexsays, where it started too early before.
#1.15.0 - 2026-11-12
#Added
- Admin screens (ADR 0024, docs/admin.md): the
adminpackage generates screens to manage an app's data from Go structs.- For each kind of record, a list with search, filters, sorting and pages, and a form to create, edit and delete one, checked against the struct's
validatetags and itsValidatemethod. admin.NewTablekeeps records in a SQLite or PostgreSQL table;admin.Storeis for data kept elsewhere.- The
admintag marks keys, listed, searchable and filtered fields, read-only fields, references to other resources, and integers that hold times. - Tern UI's
Admincomponent renders them, in a route of the app's own, so they work without JavaScript.
- For each kind of record, a list with search, filters, sorting and pages, and a form to create, edit and delete one, checked against the struct's
- Shelf, the tutorial's app, has admin screens for its links and accounts.
tern add(guide): adds plugins to the app'smain.go, by name or any Go package with a plugin, keeping its formatting and comments; it gets the module when it isn't Tern's own. tern.land/plugins lists Tern's plugins and the community's, fromplugins/registry.json, where a test checks that every entry compiles.- Editors (docs/editors.md):
tern lspis a language server that goes from a page'sdatato its Go load and back, finds routes as workspace symbols, and showstern check's errors when files are saved. Extensions for VS Code (with a Routes view) and Zed start it; Neovim and Helix can too. - Collaboration in live components (docs/live-components.md):
goscript.Roomshows who is looking at the same thing, each with state of their own, such as which field they're in;goscript.Docsis a shared record that people change together, merged field by field, loaded and saved through functions of the app's own. Both work across server processes through the broker, andtern checktypes them in the markup. - The goscript example has a shared note.
- Feature flags (docs/flags.md): the
flagspackage turns features on for some users first, decided on the server: a kill switch, users and teams who always get a feature, and a rollout to a share of users, teams or visitors that keeps everyone who had it as it grows. Flags live in the database and are read from memory;f.Admin()adds a screen for them to the admin panel, and the dev tools show which flags each page checked. - Shelf has a
searchflag, on for everyone, and a screen for its flags. - Search (ADR 0025, docs/search.md): the
searchpackage indexes an app's data in SQLite (FTS5), PostgreSQL (tsvector) or memory, and finds it as people type: words match as prefixes, without case or accents, within a scope such as one user's documents. Hits come with snippets that mark the matches, as text rather than HTML. SearchBoxandHighlightin Tern UI: results as the user types, with keyboard navigation and a hotkey, and a results page that works without JavaScript.admin.AfterSaveandadmin.AfterDelete, to keep a search index or a cache in step with what admins change.- Shelf searches each user's links.
- Streaming endpoints (guide):
func POST(ctx *kit.Ctx, in Ask, out *kit.Stream[Delta]) errorsends values as they come, such as a model's reply as it's written, as server-sent events.$api'sstreamreads them as a typed async iterator, and throws the server's error as anApiError. Leaving the loop stops the endpoint. The OpenAPI document describes them. - Dev tools in
tern dev(docs/dev-tools.md): a button on every page opens a panel with the route, its files at the line of each load, the data each load returned, and each request's spans and logs. Errors from the server, the browser and the build open over the page, with the app's frames, the source around each and a button that opens it in your editor (TERN_EDITOR, or Zed, Cursor or VS Code).
#Changed
- Native renders allocate less: strings and numbers become values from slabs rather than one box each, on the paths that make most of them (HTML, page data, string helpers). The 100-row table renders with 40% fewer allocations (5,501 to 3,285) and 5% faster (268 to 254 µs).
#1.14.0 - 2026-11-12
#Added
- Apps for assistants (ADR 0023, docs/mcp-server.md): the
mcppackage serves an app's tools to AI assistants over MCP, at/mcp.- Tools are Go functions with typed arguments, whose
jsonnames,descriptiontags andvalidaterules become their schema and are checked before they run. They run as the signed-in user. - It speaks MCP 2026-07-28 and the 2025 versions on one endpoint, without sessions.
- With
plugins/oidc, users approve each assistant on the app's consent screen, and tokens are bound to the MCP server.
- Tools are Go functions with typed arguments, whose
plugins/oidc: Client ID Metadata Documents (ClientMetadataDocuments), so clients such as assistants register themselves, fetched with protection against server-side request forgery. Resource indicators (RFC 8707), withAccessToken.Forto check a token's audience. Authorization server metadata at/.well-known/oauth-authorization-server(RFC 8414).kit.ValidateInputandCtx.UserError, for code that answers requests its own way.- A tutorial on tern.land that builds a complete app, one step per page: a reading list with a database, validated forms, accounts, tests and a deploy. The finished app is examples/shelf.
#Fixed
- Security:
plugins/oidcskipped the consent screen for an app that asked for no scopes, since "every requested scope was approved" was true of none. Consent is now always needed once per app, unless it'sTrusted. npm testin a new project works before its first build: it runstern build, which the tests need to render pages, instead oftern gen.
#1.13.0 - 2026-11-12
#Added
- Smaller binaries: when every page compiles to Go,
tern buildleaves the JavaScript engine out, about a tenth of the binary.TERN_KEEP_ENGINE=1keeps it. - Emails render natively, as pages do, with the engine as the fallback. Live components already did, within their pages.
#Fixed
- Error groups are listed most recent first on Windows too, where two reports moments apart could get the same time.
#1.12.0 - 2026-11-12
#Added
- Rendering on demand (ADR 0021, docs/caching.md):
- Pages:
const Revalidate = 300renders a page on its first request and serves it from the cache, rendering it again in the background once it's stale. Cached pages are rendered without the visitor's cookies, so nothing personal can be cached. - Tags:
c.Tagnames the data a page used, andc.Revalidate(tags...)refreshes everything that used it, in every process. - The
cachepackage: in memory, on disk or in Redis (cache/redis, with no dependency), with tags, stale-while-revalidate and one call per key at a time.cache.Getkeeps load results inc.Cache(). - Cache headers:
const CacheControlsetsCache-Controlfor a route's pages and data, for CDNs. - No JavaScript:
const CSR = falsesends a page without scripts.
- Pages:
#1.11.0 - 2026-11-12
#Added
- Teams in
accounts(ADR 0019, docs/accounts.md):- Teams: users make teams, belong to several and switch between them. Each session remembers the current one:
accounts.CurrentTeamandacc.RequireTeam. - Invitations by email: only the invited address can accept, and accepting verifies it.
- Roles: owner, admin and member are built in, with fixed rules, and a team never loses its last owner. Apps add roles of their own and check them with
rbacthroughaccounts.TeamRoles. - The app's data:
Options.OnDeleteTeamdeletes it before a team goes.
- Teams: users make teams, belong to several and switch between them. Each session remembers the current one:
- An audit log in
accounts: sign-ins and failed ones, password, email and two-step changes, passkeys, sessions, and team and member events, with the IP and device. Users see their own (acc.Activity), team admins their team's (acc.TeamActivity), and support tools anyone's (acc.AuditEntries). Apps add events withacc.Audit. - Team screens in Tern UI:
NewTeam,TeamSwitcher,TeamMembers,TeamInvitations,TeamSettings,AcceptInvitationandActivityLog. - The saas template has teams: everyone gets a team at sign-up, projects belong to the team, and
/settings/teamhas the members, invitations and the team's activity. The security page shows the user's own activity, and the dashboard counts members. - Billing with Stripe (ADR 0020, docs/billing.md):
- Plans in Go, with Stripe's prices, trials, seats per member, limits and features.
- Subscribing:
b.Checkoutstarts Stripe Checkout, andb.Portalopens the customer portal. - Webhooks: verified, recorded once, and processed as jobs that fetch the subscription from Stripe, so repeated, late and out-of-order events are safe.
- Limits:
b.Checkandb.Hascheck a plan's limits and features in loads and actions, with messages for forms and a 402 for endpoints. - Screens: Tern UI's
PricingTableandBillingSettings.
- The saas template bills teams: a free plan with limits and a per-member Pro plan with a trial, a
/pricingpage,/settings/billingwith the usage, and seats kept in step with members. Without Stripe's key, every team is on the free plan. kittest.Client.Browserreturns a second client of the same app, with no cookies of its own.
#Fixed
- Link buttons in Tern UI's sign-in and account cards kept the cards' link color, so their text was hard to read.
scripts/test-template.shno longer stops the shell that runs it when a step fails before the server starts.
#Changed
accounts.Migratecreates the teams and audit tables, and adds ateam_idcolumn totern_sessions.
#1.10.0 - 2026-11-11
#Added
- Tracing (ADR 0017, docs/tracing.md):
- Spans: the
tracingpackage records requests as trees of timed spans and exports them over OTLP to any OpenTelemetry backend. - kit:
kit.WithTracermakes kit trace each request, with spans for its loads, actions, endpoints and server render, and it continues a trustedtraceparent. - HTTP and SQL:
tracing.Transporttraces outgoing HTTP requests and passes the trace on.tracing.OpenDBtraces SQL queries. - Jobs: a job runs in the trace it was queued in.
- Logs: records logged inside a span carry its trace and span IDs.
- Sampling:
TERN_TRACE_SAMPLEkeeps a fraction of traces.
- Spans: the
tern devtraces every request and shows each one's spans as a waterfall in the log view.- Error reports (ADR 0018, docs/errors.md):
- What's reported: unexpected errors and panics from loads, actions, endpoints, hooks, live components and jobs, plus uncaught errors and unhandled rejections in the browser.
- Grouped by cause and rate-limited, with the stack, request, route, trace, release and user.
- Sent to Sentry or any service that takes its protocol.
- Wiring:
kit.WithReporterturns it on.report.WithStackandreport.Errorfgive returned errors a stack. - In dev:
tern dev's log view lists the groups.
- Testing (docs/testing.md):
- Loads and actions:
kit/kittestcalls loads, actions and endpoints from Go tests, with a request made of options (path, form, JSON, route parameters, layout data) and the app's hooks run. Helpers read redirects, failures and statuses. - A database per test:
kittest.SQLitegives each test its own in-memory database, migrated. - Pages:
kittest.NewClientruns the whole app and queries its HTML with CSS selectors. - Signed-in users:
accounts.ActAsmakes a user the signed-in one for a request. - Building blocks:
kit.App.NewCtxmakes a request's Ctx, which kittest builds on.
- Loads and actions:
- New projects come with tests: Go tests of their pages (
npm test), and Playwright set up with a browser test of the home page (npm run test:e2e).
#Changed
- The
tern_jobstable has atracecolumn, whichMigrateadds to existing databases.
#1.9.0 - 2026-11-11
#Added
- Logging (ADR 0014, docs/logging.md).
logging.Newreturns a*slog.Loggerwith these features:- Request context: kit gives every request an ID, kept from a trusted
X-Request-Idor made new, and returns it asX-Request-Id. Records logged with the request's context include it, plustrace_idandspan_idfrom a trustedtraceparent. c.Logger()binds the ID and the route;c.LogWithandlogging.Withadd attributes of your own.- Passwords, tokens, keys, cookies and other secrets are redacted by key, including in headers and URLs, and
logging.Secretalways is. - Optional sampling of floods.
TERN_LOG_LEVELandTERN_LOG_FORMAT.
- Request context: kit gives every request an ID, kept from a trusted
- Logging sinks that ship records in batches with retries, without blocking:
logging/otlp(OpenTelemetry over HTTP),logging/loki,logging/datadogandlogging/file(JSON lines, rotated). A sink of your own is one method. - HTTPS without a proxy:
kit.WithAutoTLS, orTERN_TLS_DOMAINS, serves HTTPS with certificates from Let's Encrypt, which the app gets and renews itself, and redirects plain HTTP.kit.WithTLStakes certificates of your own. HTTP/2 is offered in both cases. - Security signals (ADR 0015, docs/signals.md):
plugins/signalsscores how automated each request looks, with reasons, from:- the JA4 TLS fingerprint, an HTTP/2 fingerprint and the order of the headers;
- how the headers fit the browser the User-Agent claims;
- an optional client check in the browser: a proof of work and three automation tells.
It also gives each browser a device ID.
signals.Of(c)returns them,Options.Blockrefuses scripted form posts, andsignals.Keymakes rate limits per client.
accounts:Options.NotifyNewDevicesemails users about sign-ins from a device their account hadn't been used on, andOnNewDeviceis called then.- kit plugins can wrap connections (
Setup.WrapConn), see TLS ClientHellos (Setup.OnClientHello) and add to a connection's context (Setup.ConnContext).kit.IsHTTPSandkit.FromTrustedProxyare exported. tern deploy(ADR 0016, docs/deployment.md) deploys to targets named intern.json:sshputs the app on a Linux server under systemd, creating its user and unit, and serves HTTPS itself when the target has domains. Restarts have zero downtime: the new release starts next to the old one on the same port, and the old one stops only once the new one is ready. Releases are kept for-rollback, and-releasesand-logswork too.flydeploys to Fly.io.containerbuilds and pushes an image, then runs the platform's deploy command.- Any other provider runs a
tern-deploy-<name>program with the deployment as JSON.
- kit tells systemd when the app is ready and when it stops (
Type=notify), and withTERN_REUSEPORT=1listens with SO_REUSEPORT, so two releases can share a port. tern buildcross-compiles apps that prerender pages: with GOOS or GOARCH set, it prerenders with a binary for the machine it runs on.tern devhas a live log view at/__tern/dev/logs: the app's records and the requests it serves, filterable by level, text and request.
#Changed
- kit adds the request's attributes to records from loggers it didn't make, and flushes the logger's sinks on shutdown. The saas template and the examples log with
logging.New.
#Fixed
tern devserves files from the workspace'snode_modules, such as fonts from@fontsource, in pnpm and npm workspaces. Tern's Vite plugin allowed only the app's own folder.tern devno longer shows pages unstyled on their first paint. Vite delivers stylesheets as JavaScript after the HTML; the dev server now puts the CSS of the page's components in the server-rendered HTML, and the client drops that copy once Vite's styles, which hot-reload, are in place.- Scroll positions: a page opened in a new history entry, from a link elsewhere or the address bar, starts at the top instead of at a position saved for an earlier page. Moving between pages jumps there at once, even where the CSS sets
scroll-behavior: smooth: the animation used to stop partway as the new page rendered.
#1.8.0 - 2026-11-11
#Added
- A full app to start from.
tern new -template saas, or "full" whentern newandnpm create ternask, creates an app with accounts built: sign-up, email verification, password resets, passkeys, two-step sign-in, and sign-in with GitHub or Google when their keys are set. It has a landing page, a dashboard with a getting-started checklist, projects as an example of per-user data in SQLite with a migration, settings for the profile, email address and password, and a security page for two-step sign-in, passkeys, devices and connected accounts. Emails go through SMTP in background jobs, or to the log, link included, in development. The bare templates are as before. tern devreads a.envfile into the app's environment; variables already set win over it.
#Changed
- Native rendering, phase 2 (ADR 0013): an uncached render of the benchmark's 100-row table takes 272 µs, from 352 µs in v1.7.0, and allocates 352 KB, from 573 KB.
- Page data reaches native renders as the loads' Go values, made into the same JavaScript values
JSON.parsewould make, without the JSON. Don't change what a load returns after it returns. - Svelte's
Renderer.pushand#collect_content, and itsattr,to_class,attr_class,stringifyandensure_array_likehelpers, run as Go. Each is used only while Svelte's source is the one it was written against, and is checked against Node. - The document is written in one pooled buffer instead of being copied at each step.
- Page data reaches native renders as the loads' Go values, made into the same JavaScript values
#Fixed
- Native rendering: String objects have their index properties, so
Object.keys('ab')is['0', '1'].
#1.7.0 - 2026-11-11
#Added
- Two-step sign-in in
accounts: codes from an authenticator app (TOTP) after the password, with recovery codes.SetUpTwoStepreturns a QR code,ConfirmTwoStepturns it on,SignInreturnsErrTwoStepRequiredandCompleteSignIntakes the code. Tern UI'sTwoStepandTwoStepCodeblocks show it. - Sign in with GitHub, Google or OpenID Connect into
accounts:plugins/oauthtakesAccounts: accin place ofOnLogin. A provider's sign-in joins the account with the same verified email, or creates one, or connects to the signed-in user;acc.Identitiesandacc.Unlinkmanage them, and Tern UI'sLinkedAccountsshows them.acc.SignInWithdoes it for any provider. acc.RequestEmailChangechanges a user's email address once they open a link sent to the new one, and tells the old one.accountslimits attempts per account and per IP, on by default: sign-ins, sign-ups, emails, passkey sign-ins and two-step codes. Past a limit, methods returnErrTooManyAttempts;Options.NoRateLimitsturns them off, for tests.- A Benchmarks workflow: every push and pull request renders the benchmark app's pages at the commit before and at the new one, and fails on a significant slowdown of more than 10%. Pushes to
mainalso run the comparison with SvelteKit. tern routes -jsonprints the routes as JSON: pattern, directory, kind, whether it has a load, and its layouts.tern devsays which routes were added and removed when route directories change.
#Changed
- Page data, action results, typed endpoint output and live component state are encoded by Tern's own JSON encoder: byte for byte what encoding/json writes, with nil slices as
[], about 6× faster, without copying the data first. An uncached render of the benchmark's 100-row table takes 17% less time. tern buildcompiles the main package, which holds the native renderer, without inlining: the dashboard example's binary is 11% smaller (45 → 40 MB) and builds faster, with renders as fast as before.- Native rendering builds the page data's objects at their final shape, and property reads that miss their cache no longer allocate.
#Fixed
- Tern UI's
SignInsaid a sign-in had failed when the user cancelled it at the provider: it now matchesplugins/oauth'sdeniedcode. - Tern UI's
Formhid the server's error on a field that lost focus without being changed, and two submits in a row raced each other: now the newest submit aborts the older one.
#1.6.0 - 2026-11-11
#Added
- User accounts. The new
accountspackage signs users up and in with a password or a passkey, and keeps their accounts in your database (SQLite or PostgreSQL). See docs/accounts.md and ADR 0012.- Email verification and password reset links, sent with
accounts.SendWith(mailer, queue)or your ownSend, in the user's language. - Sessions in the database:
acc.Sessionslists the devices a user is signed in on, andacc.Revokeandacc.SignOutOtherssign them out. Changing or resetting a password signs out the others. - Passkeys (WebAuthn, ES256, EdDSA and RS256):
registerPasskey()andsignInWithPasskey()from@ternkit/tern, over endpoints the plugin mounts. - Passwords hashed with argon2id. bcrypt hashes from another system still work and are rehashed at the next sign-in.
accounts.Current(ctx)andacc.Require(ctx)in loads and actions.accounts.FormErrorturns a wrong password or a taken email into a form error.- Tern UI blocks for each screen:
ForgotPassword,ResetPassword,DevicesandPasskeys, and a passkey button onSignIn. - examples/accounts runs every flow, passkeys included, in browser tests.
- Email verification and password reset links, sent with
- Tern's own texts through
src/locales. Tern UI's labels and the validation messages are messages with keys starting withtern., with English built in. Translate them in your locale files like your own messages; what a locale leaves out stays English.kit.FormActionandvalidate()translate validation messages for the request's locale, so the server and the browser agree. The guide lists every key. - Native rendering compiles everything that was left to the JavaScript engine: dynamic
import(), BigInt, async generators,for awaitandnew.target. It also has every built-in QuickJS has:Object.groupByandMap.groupBy, the Set methods, iterator helpers andIterator,WeakRefandFinalizationRegistry,Array.prototype.copyWithin,String.prototype.isWellFormedandtoWellFormed,Symbol.keyFor,escapeandunescape, and the RegExpd(match indices) andvflags. No example leaves anything to the engine.
#Changed
- A nil Go slice is sent as
[]instead ofnullin load data, action results, deferred values, typed endpoint output and live component state, so slices are typedT[]instead ofT[] | null. Combobox,SelectMenu,DatePicker,Slider,Switch,ToggleandToggleGroupare checked as the user changes them inside aForm, like inputs.- A page whose render throws an error that the JavaScript engine throws too keeps rendering natively. Only a page native rendering can't do goes to the engine from then on.
- Renders are faster: the benchmark table renders in 259 µs, from 320. The page data is only searched for deferred values when it has some, and native renders return their result without encoding it as JSON.
#1.5.0 - 2026-11-11
#Added
- Native server rendering.
tern buildcompiles the server renderer (your components, Tern's runtime, Svelte's server runtime and libraries) to Go, so pages render without the JavaScript engine: the benchmark app's 100-row table renders in 0.3 ms instead of 2.9 ms, and serves 12× more uncached requests per second, more than SvelteKit on ten Node processes. Renders run in parallel on every core.- The HTML is byte for byte what the JavaScript engine renders. What the compiler doesn't support (async generators,
for await, dynamicimport(), BigInt) is listed in.tern/native/report.txt; pages that use it, and any page whose native render fails, render with the engine as before. TERN_NATIVE_VERIFY=1renders every page both ways, serves the engine's HTML and logs differences.TERN_NATIVE=0,"native": falseintern.jsonorkit.WithoutNativeSSR()turn it off.- Add
tern_native.gen.goto.gitignore: it is generated by every build.
- The HTML is byte for byte what the JavaScript engine renders. What the compiler doesn't support (async generators,
#Changed
- Server rendering reuses the page data it parsed instead of copying it, in the browser and the JavaScript engine too.
kit.WithSSRWorkersdefaults to one worker per CPU when pages render natively. The JavaScript engine still uses at most 4.
#1.4.0 - 2026-11-10
#Added
- Form validation from Go struct tags.
kit.FormActiondecodes a post into a struct and checksvalidate:"required,min=3,email,…"before the action runs, failing with akit.Invalidof errors by field and the posted values (never passwords).tern genwrites the same rules to./$forms,validate()from@ternkit/ternchecks them in the browser with the same messages, and Tern UI's newFormchecks them as the user types and shows each error on itsFieldbyname.kit.FieldErrorandkit.FormErrorfail with a message on a field or on the whole form. Shared test cases keep Go and TypeScript in step. - The complete Tern UI.
- Overlays:
Popover,HoverCard,AlertDialog,Sheet,Drawer,ContextMenuandMenubar. Menus gain submenus, checkbox and radio items, shortcuts and labelled groups. - Inputs:
Combobox(with server search),SelectMenu,Command(and a ⌘K palette),Calendar,DatePicker(with ranges),Slider(with ranges),PasswordInput,InputOTP,Toggle,ToggleGroup,InputGroup,ButtonGroupandLabel. - Layout and data:
Sidebar,NavigationMenu,Breadcrumb,Pagination,DataTable,Table,Resizable,Carousel,Collapsible,ScrollArea,AspectRatio,EmptyandItem. - Charts:
LineChart,AreaChart,BarChart,DonutChart,StatandSparkline, with a palette validated for color blindness (--tern-chart-1to--tern-chart-8), values read from the keyboard, and a table view. - Blocks:
SignIn(OAuth providers and a password form),SignUpandConsentScreen.
- Overlays:
tern ui add <Component>copies Tern UI components, and the helpers they use, intosrc/lib/uito change them;tern ui listlists them. The MCP server has it asui_add.npm create tern@latest(the newcreate-ternpackage) runs the matchingtern new.tern newwithout a directory asks where to create the project, which template to use, whether to style it with Tern UI, and whether to create a git repository and runnpm install;-ui,-gitand-installdo the same without questions, and flags may follow the directory.examples/analytics: a self-hosted, privacy-friendly web analytics app. A cookie-free tracking script, a live dashboard with charts and tables, sign-in, hourly totals and pruning as jobs, a weekly email report, and a stats API, in one binary.ctx.ClientIP()andkit.ClientIP(r)return the client's address.kit.WithTrustedProxiesnames the reverse proxies whoseX-Forwarded-Foris believed, read from the right so clients can't spoof it. The rate limiting plugin uses it.plugins/oidc:ConsentURLsends users to a consent page of the app, andConsentRequestreturns what it shows, checked as the authorize endpoint checks it.- The server renderer's
Intl.NumberFormatsupportssignDisplayandcompactDisplay: 'long', matching Node'sen-USoutput. - The guide has recipes for signing in with a password and for validating forms.
#Changed
- Typed endpoints check
validatetags on their input, askit.FormActiondoes, and answer 400 when they fail. Inputs with such tags were accepted unchecked before. - Tern UI's menus and tooltips open in the top layer and stay on screen, so containers with
overflowno longer clip them.Tooltiptakesleftandrighttoo. - Once
kit.WithTrustedProxiesis set,X-Forwarded-Protois only believed from those proxies. - Compact numbers during server rendering round like browsers do (
12K, not12.3K;999,999is1M).
#Fixed
- A
Fieldin a stretched grid row no longer grows its label, so controls in a row line up. - Two compiled
ternbinaries had been committed to the repository by accident; they are removed and ignored. - The guide's hook example called
sessions.User, which doesn't exist.
#1.3.0 - 2026-11-10
#Added
- Live components are supported, with no flag:
<script lang="go" live>runs a component's Go on the server. See docs/live-components.md.- Concurrency: each component runs on its own goroutine, so components run in parallel and a slow Func delays only its own. Work from other goroutines goes through
c.Do,c.Goandc.Every. - Funcs: they may take a
context.Contextand return a value and an error.goscript.Failerrors reach the page; other errors and panics are logged and reported generically. - In the markup:
- each Func has
.pending,.error,.optimistic(values, …)and.debounce(ms) livegives the connection state, bindings (bind:value={live.bind.name}for a State declared withgoscript.Bind) and props
- each Func has
- Props and events:
goscript.Props[T]follows the parent's props without remounting, andc.Emitcalls the parent'son…handlers. - Access control:
c.Rejectrefuses a mount, such as for visitors who aren't signed in. It works during server rendering too. - Updates and reconnects:
- changes go in one message per batch, with patches for lists and objects
- pages reconnect with backoff, and
BindandRestorevalues survive reconnects and deploys
- Sharing across processes:
goscript.Shared,goscript.Presenceandgoscript.Publish/Subscribereach every process through a broker, set withkit.WithLiveBroker.realtime/pgbrokeris a broker on PostgreSQL LISTEN/NOTIFY. - Limits:
kit.WithLivesets limits: instances per page, call rate, send buffer and ping interval. - Testing:
goscript/livetesttests components in Go without a browser.
- Concurrency: each component runs on its own goroutine, so components run in parallel and a slow Func delays only its own. Work from other goroutines goes through
tern checkgenerates code and runssvelte-check, with the markup of Go components checked against types generated from their Go code. Starter templates'npm run checkuses it.- The status example's admin dashboard is a live component.
@ternkit/tern/ui, a component library with Tern's own design, themeable with CSS custom properties, in light and dark mode. See docs/ui.md.- Components: Button, Field with Input, Textarea and Select, Checkbox, RadioGroup, Switch, Dialog, Tabs, Menu, Tooltip, Accordion, toasts, Alert, Progress, Spinner, Card, Badge, Avatar, Skeleton, Separator and Kbd.
- Accessibility: they follow the WAI-ARIA patterns and work with the keyboard, and
examples/uiaudits them with axe in CI.
tern mcpserves the Model Context Protocol for AI coding tools. Its tools cover project information, routes and their types, live components, code generation, checks, tests, builds, scaffolding routes, live components and migrations, and Tern's docs for the version in use. New projects include a.mcp.json. See docs/mcp.md.
#Fixed
- Server rendering looped forever on some pages with two-way bindings: the minified server bundle was not strict mode, so a renamed block-level function could shadow a variable. The server and service worker bundles are now strict.
#Changed
- Plugins' hooks run before the app's own hooks, so app hooks can use sessions and other plugin state, as the guide's example does.
tern devrebuilds the server only when a live component's Go code changed, not on markup edits.- Breaking, for the experimental Go component API:
GlobalSignal.Getno longer subscribes; read the signalShowor the newUsereturns.goscript.MountLivetakes options.Effectno longer runs during server rendering.
#1.2.1 - 2026-11-10
#Changed
@ternkit/ternis published to npm with trusted publishing and provenance; the release workflow holds no npm token.
#1.2.0 - 2026-11-10
#Added
- Translated URL paths:
"$routes": {"projects": "projekte"}in a locale file makes/de/projektematch/lang.optional/projects, androute()builds localized paths when givenlang. - Remote images:
<Image src="https://…" width height>withplugins/images, which fetches images from allowed hosts, resizes them to the srcset's widths and caches them. tern new -template blog|api: a Markdown blog with prerendered posts and an RSS feed, or typed JSON endpoints with OpenAPI and a typed client. CI builds and runs every template.- Coming from SvelteKit: a guide that maps SvelteKit's files, APIs and modules to Tern's.
- Experimental live Go components,
<script lang="go" live>: the samegoscriptAPI running on the server over one WebSocket per page, server-rendered with real state per request (props andLocals), withgoscript.Globalstate shared across visitors,OnDestroyandRendering. - Experimental
<script lang="go">("experimental": {"goScript": true}intern.json): component logic in Go with runes (goscript.State,Derived,Effect,Func), compiled to one WebAssembly module per app, with build-time initial state for server rendering. See docs/experimental-go-script.md for measurements and limits. - Server rendering formats
Number.prototype.toLocaleStringand theDatelocale methods like browsers (en-US), so their output hydrates. - Typed endpoints:
func GET(ctx *kit.Ctx, in Query) (Out, error)decodes the query, path parameters (path:"id") or JSON body, runs an optionalValidate(), and writes the output as JSON.tern genwrites an OpenAPI 3.1 document of them (served withkit.WithOpenAPI) and a typed client,api('POST /api/projects', { body })from$api, which throwsApiErroron failure. - File storage: the
storagepackage stores files on disk or in S3-compatible buckets (SigV4, presigned URLs), receives uploads with size limits and content sniffing (storage.Receive), makes unguessable keys, and serves files with expiring signed links, range requests and safe headers (storage.Serve). ctx.SetBodyLimitraises the body limit for one request, such as an upload.- Email: Svelte components in
src/emailsrender on the server into a subject, HTML with the component's styles, and a plain-text version (app.RenderEmail). Themailpackage sends them over SMTP (STARTTLS or implicit TLS), with aLogtransport for development andmail.Jobfor retries. Undertern dev,/__tern/emailspreviews every template with itspreviewprops. plugins/oauth: sign in with GitHub, Google or any OpenID Connect provider, with PKCE, state and nonce in an encrypted cookie and verified ID tokens.plugins/oidc: an OAuth 2.0 authorization server and OpenID Connect provider with discovery, JWKS, consent, the authorization code flow with PKCE, refresh token rotation with reuse detection, client credentials, userinfo, revocation and introspection, RS256 JWT access tokens andprovider.Bearerfor APIs. Its state lives in aMemoryStoreor aSQLStore(SQLite, PostgreSQL).rbac: roles, permissions with wildcards and included roles,Require,Canand a hook for whole sections.Setup.Endpoint(plugin routes with aCtx, after hooks) andSetup.AllowCrossOrigin.- examples/status: a self-hosted status page and uptime monitor (HTTP, TCP, DNS and certificate checks, a live dashboard, a public page with 90-day uptime, incidents, webhook, Slack, Discord and email notifications, and badges) built on migrations, jobs, realtime and sessions.
- Background jobs: the
jobspackage stores jobs in SQLite or PostgreSQL, runs them with retries, backoff, timeouts, delays, unique keys and transactional enqueueing, and runs cron and@everyschedules once across instances.q.Stats,q.Listandq.Retrysupport admin pages. - Database migrations: the
migratepackage applies<version>_<name>.sqlfiles in transactions on startup (migrate.Plugin), detects edited migrations, and supports-- +downsections.tern migrate new <name>creates a file. Setup.OnStartfor plugins that start work with the server, andApp.Startfor custom servers. Start functions don't run while prerendering.- Realtime:
ctx.EventStreamfor server-sent events andctx.WebSocketfor WebSockets (same-origin by default), both ending cleanly on shutdown; therealtimepackage'sHubpublishes to subscribers through a pluggableBroker;stream()andsocket()from@ternkit/ternkeep the latest message in reactive state and reconnect. - Markdown pages: a
+page.mdroute is rendered to HTML bytern gen, with its front matter'stitleanddescriptionin the page head. - Content collections: the
contentpackage reads Markdown fromsrc/contentwith typed YAML front matter, rendered with GitHub-flavored Markdown, heading IDs, footnotes and highlighted code.src/contentis embedded in the binary; drafts show only undertern dev.ctx.Content()returns the directory. Entriesmay take actx *kit.Ctx, for example to list a collection's slugs.- A render cache: server-rendered HTML is cached by the page's data and URL (32 MiB by default,
kit.WithSSRCache), and concurrent renders of the same page are coalesced. - Rendering VMs are replaced after 10,000 renders, which bounds memory held by libraries across renders.
- More web APIs while server rendering:
TextEncoder,TextDecoder,atob,btoa,crypto.getRandomValues,crypto.randomUUID,structuredClone,Event,CustomEvent,EventTarget,AbortController,AbortSignal,performance.now(), and timers that never fire.fetchrejects with an explanation. - A compatibility suite renders 23 popular libraries in Tern on every push and checks the HTML matches Node's.
tern({ ssrBrowserBuilds })bundles a package's browser build for server rendering;nanoiduses it by default.
#Changed
- The server bundle resolves libraries without the
nodeexport condition, so they use portable builds instead of ones needingBufferornode:crypto. - Server rendering defaults to at most 4 workers (
kit.WithSSRWorkersstill overrides it). The VMs share a lock in the C runtime, so more workers rendered slower; uncached throughput is up by half.
#Fixed
- Under
tern dev, the app's own WebSocket endpoints were proxied to Vite; only Vite's connection is now. - Server rendering no longer leaks memory. The
URLpolyfill stored its state in a way QuickJS never frees, losing about 5 KB per render; Tern now ships its ownURLandURLSearchParamsand no longer depends on core-js.
#1.1.0 - 2026-11-10
#Added
- Responsive images:
import hero from './hero.jpg?image'encodes AVIF, WebP and fallback versions at several widths with sharp, and<Image>from@ternkit/ternrenders them as a<picture>with width, height,sizesand lazy loading.?w=400,800&imagepicks the widths. - Translations: JSON message files in
src/locales,t()with typed keys and ICU plurals and selects in components,ctx.Locale()andctx.T()in Go, locale detection from alangparameter, thelocalecookie orAccept-Language,setLocale()to switch without a reload, and%tern.lang%inapp.html. The browser loads only the current locale's messages. - More built-in plugins:
plugins/sessions(encrypted cookie sessions with flash messages and secret rotation),plugins/metrics(Prometheus metrics labeled by route pattern),plugins/ratelimit(per-client token buckets) andplugins/cors. - Service workers:
src/service-worker.tsis built to/service-worker.jsafter prerendering and registered in production. The$service-workermodule lists the build's assets, static files and prerendered pages, with a version for cache names.serviceWorker.registerintern.jsonturns automatic registration off. Setup.TrustOriginfor plugins that allow cross-site requests.
#Changed
ctx.SetCookiereplaces a cookie set earlier in the same request instead of sending both.
#1.0.0 - 2026-11-09
Tern 1.0 is stable: the APIs described in docs/stability.md follow semantic versioning from here on.
#Added
- A plugin system:
kit.WithPluginsand thekit.Plugininterface. A plugin'sSetupcan add hooks, middleware, matchers, HTTP handlers served before routes, HTML in every page's<head>(with the CSP nonce), and shutdown work. - The stability promise, and decision records for selective reloading and plugins.
- Public environment variables:
PUBLIC_variables in the server's environment are available in components asenvfrom@ternkit/tern, during server rendering and in the browser. - Base paths:
{"basePath": "/app"}in an optionaltern.jsonserves the app under a URL prefix. Assets,%tern.assets%,route(), client routing, prerendering andkit.Redirectinclude it;ctx.Pathandctx.BasePathhelp with links built in Go. - Built-in plugins:
plugins/health(liveness and readiness with checks),plugins/requestlog(structured access logs),plugins/compress(gzip that keeps streaming working) andplugins/secure(security headers).
#0.4.0 - 2026-11-09
#Added
examples/dashboard: a shadcn-svelte, Tailwind 4, bits-ui, layerchart and SQLite dashboard with sidebar navigation, dialogs, selects, dropdown menus, toasts, charts, streamed data and dark mode, covered by end-to-end tests.- GET forms (such as search boxes) navigate on the client, keeping focus and scroll when they filter the current page.
- Server rendering supports libraries that read
process.envor useIntlwhile loading:process.env.NODE_ENVis replaced at build time, and the renderer provides aprocessstub and anen-USIntl(NumberFormat,DateTimeFormat,PluralRules,Collator,RelativeTimeFormat,ListFormat,Locale,Segmenter). - A documentation website at https://unfield.github.io/Tern/, built from
docs/by a small generator inwebsite/and deployed by GitHub Actions; a logo. - Tests: a
tern devintegration test (rebuilds, the error page, recovery, shutdown), a starter-template test that creates, type-checks, builds and runs a project, client unit tests with Vitest, and benchmarks (docs/benchmarks.md). - Typed links:
tern genwrites a$routesmodule, androute('/projects/[id]', { id })builds URLs with the pattern and parameters checked by TypeScript. - Navigation hooks:
beforeNavigate(can cancel, including leaving the page),onNavigate(can delay rendering, for view transitions) andafterNavigate. - Selective reloading: loads record what they read (
ctx.Param,ctx.URL,kit.Parent, and keys from the newctx.Depends). On client navigation, layouts whose inputs did not change are reused instead of rerun, andinvalidate(key)reruns only the loads that depend onkey. - Content Security Policy:
kit.WithContentSecurityPolicyandkit.WithContentSecurityPolicyReportOnlyadd a per-request nonce toscript-src. - Prerendered endpoints: a
GETendpoint withconst Prerender = true, such assitemap.xml, becomes a static file at build time. - A redesigned CLI: colored help, a
tern devbanner with local and network URLs and timestamped rebuilds,tern buildsteps with spinners and a size summary, and styledroutes,genandnewoutput. Color turns off when output is not a terminal;NO_COLORandFORCE_COLORare honored. tern versionalso prints the Go version and platform; untagged builds reportdev.- If the server bundle fails to load, the error includes the JavaScript stack trace.
- Generated types include Vite's client types, so CSS and asset imports type-check.
#Fixed
tern devwaits for Vite before serving, so the first pages are server-rendered.tern devrefuses to start when anothertern devis already running for the same project, instead of both overwriting the same build output.- The Vite plugin uses
server.ws.clientPortinstead of the deprecatedserver.hmr.clientPort. - An action with an
anyresult no longer turns every field offormintounknown.
#0.3.1 - 2026-11-09
#Changed
- The npm package is now
@ternkit/tern(it was never published as@unfield/tern). Update imports to@ternkit/ternand@ternkit/tern/vite.
#Fixed
- Build scratch files from
svelte-packageare no longer committed.
#0.3.0 - 2026-11-09
The first release. It covers the v0.1, v0.2 and v0.3 milestones of the roadmap.
#Added
- Route scanning:
+page.svelte,+layout.svelteand+error.svelte, plus each route directory's Go package (Load,Actions,LayoutLoad,GET,POST, …). Dynamic directories use Go-safe names:slug.param,id.param.matcher,lang.optional,path.restandapp.group. All problems are reported at once, including conflicting routes, and SvelteKit-style names get a suggested rename. - URL matching by route priority, with correct handling of encoded characters and trailing slashes.
tern routescommand to list an app's routes.kit, the server runtime: concurrent layout and page loads withkit.Parent, form actions withkit.Failand enhanced JSON responses, endpoints, hooks, middleware, matchers (integeranduuidbuilt in), cookies with secure defaults, cross-origin request protection, body limits, panic recovery, immutable asset caching with ETags and gzip, and graceful shutdown.tern gen: generatestern.gen.go, the client route manifest, and TypeScript types for every route from the Go types of its loads and actions. Signatures are checked with errors at the source position.@ternkit/tern, the client runtime and Vite plugin: client-side routing over__data.json,pageandnavigatingstate,goto,invalidateAll,preloadData,enhanceandapplyAction, scroll restoration, focus management and route announcements.tern dev: one port for the app and Vite, hot module replacement, automatic Go rebuilds and browser reloads, and an in-browser error page for build errors.tern build: a single binary with the client embedded and precompressed.tern new: a starter project that pins the CLI withgo get -tool.examples/todoswith Playwright end-to-end tests.- Streaming:
kit.Deferstarts slow work without holding up the page. The value is aPromisein Svelte; it streams as an inline script on full page loads and as newline-delimited JSON on client navigations. - Prerendering: pages with
const Prerender = true(and anEntriesfunction for dynamic routes) are rendered to static HTML and data bytern buildand served with ETags. tern newprojects include a multi-stageDockerfileproducing a distroless image.- The guide and the deployment guide.
- Server-side rendering with hydration. Pages render in a pool of embedded QuickJS VMs (pure Go, no cgo) with per-render timeouts and memory limits, and fall back to client rendering if a render fails. Development renders through Vite. Opt out per page with
const SSR = falseor app-wide withkit.WithoutSSR(); tune withkit.WithSSRWorkersandkit.WithSSRTimeout.