* feat(frontend): guided setup wizard for data tables
On Cloud a data table cannot use the Windmill instance database, so a new
workspace hit a dead end: an alert telling the user to go find a PostgreSQL
resource somewhere else. Setting one up meant three disconnected places, and the
connection could only be tested after the config had already been saved.
Adds a three-step wizard (choose a database -> set it up -> name it) reached from
the data tables settings page:
- Supabase: signs in via the existing supabase_wizard OAuth client and creates
the project from inside Windmill. Because db_pass is an input to project
creation, Windmill sets the password and the user never visits a dashboard.
- Your own database: picks an existing postgresql resource, or adds one with a
connection string through the form that already supports it.
- Windmill database: hands back to the inline row editor, since instance
databases are provisioned by a superadmin.
Verifying access is no longer a step the user takes: Continue runs the check and
passing it is what advances the wizard, so a database that cannot create tables
never reaches the workspace config.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: pin ee-repo-ref to the Supabase provisioning endpoints
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): do not claim the database is ready when its check failed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: address review findings on the data table wizard
- The Supabase create branch advanced on `provisioning === 4` without consulting
the check it had just run, so a role that cannot create tables could reach
Finish. It now blocks and offers Try again.
- Retrying no longer mints a fresh secret variable + resource each time: the
credentials are only re-created when the password actually changed.
- The generated password is captured before the create call rather than after,
since a throw there can still leave a project behind.
- On a failed provision the project list is refreshed, so the just-created
project can be picked up from the other tab instead of provisioning a second.
- Finish refuses a name that already belongs to another data table, which
previously repointed it at the new database.
- Secrets go to the acting user's namespace instead of a literal `u/admin/`.
- The progress list no longer ticks "Created on Supabase" before the request is
sent, and does not claim the database is ready when its check failed.
- The wizard's resume state is cleared when it closes, so reopening after an
abandoned OAuth round trip is not stuck on step 2.
- The OAuth callback shares the session-storage key rather than repeating it.
- SupabaseConnect uses the shared provisioning helpers instead of a fork.
- Restores the doc comment displaced onto TestDataTableResourceQuery.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): simplify Alert layout and balance its vertical padding
The body was rendered by two near-duplicate branches, each wrapping the text in an
extra div only to hang a margin on it, and the margins disagreed: the collapsible
branch spaced above with mt-2, the static one below with mb-2. Since isCollapsed
defaults to true, every non-collapsible alert took the static branch, so titled
alerts read as 24px of space below the text against 16px above -- visibly
off-centre -- with the title and body flush against each other.
Collapse both branches into one and drop the margins; the container's own padding
now sets top and bottom equally, with a small gap under the title row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): only offer Supabase when its OAuth client is configured
The wizard offered the Supabase card unconditionally, so on an instance whose
superadmin never configured a supabase_wizard client -- or whose backend is built
without the oauth2 feature, which compiles the whole /api/oauth router out -- the
card dead-ended at a 404. Gate it on listOauthConnects, the same check
ApiConnectForm already makes, fetched on open so configuring the client mid-session
does not require a reload.
Also drop the Supabase project ref from the existing-project cards: it is an opaque
identifier that means nothing outside Supabase's own dashboard URLs. Show the region
instead, plus a status word when the project is not healthy, since a paused project
is the one case where the connection check fails for a reason unrelated to the
password.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): run the Supabase OAuth leg in a popup
A full-page redirect unmounts the wizard, so anything the user does on Supabase's
side -- signing in, confirming an email, browsing their dashboard -- leaves them
with nothing pointing back at Windmill, and the wizard had to park its state in
sessionStorage to survive the trip.
Open the connect endpoint in a popup instead. The modal stays on screen throughout
and the callback hands the token back through postMessage rather than navigating.
The parked-state path stays as the fallback for browsers that block the popup.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): scope the connection check to the choice that produced it
A failed check stayed on screen when the user switched Supabase mode or picked a
different provider, so a fresh tab opened showing an error about a database it had
nothing to do with. Clear the report and the error on both switches; re-clicking the
tab already selected leaves an error the user is reading in place.
Also polish the Supabase step: project cards get the provider-card treatment (icon,
p-3, flex column) instead of a hand-rolled variant whose block layout left more
padding above the name than below; form labels settle on text-emphasis; and the
signup link sits under the primary button for anyone who does not have an account
yet.
Drop the "free" badge and the "Free on Supabase" line -- every option in the wizard
is free, so neither told the user anything -- and say what the Supabase card
actually does now that connecting an existing project is the default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(frontend): one setup checklist and one Supabase step for every host
The data table wizard, the instance database modal and the resource drawer each had
their own version of the same two interactions, and they had already begun to drift:
the wizard's Supabase resource shape was rebuilt by hand in the drawer, and the
instance checks rendered with no notion of a step being in flight.
SetupChecklist replaces LoggedWizardResult, whose only consumer was the instance
modal. It adds the running state that component lacked, so a list driven by an
endpoint that reports nothing until it returns still shows where it is. Both the
instance checks and the Supabase provisioning stages render through it.
SupabaseProjectStep owns picking or creating a project, and useSupabaseOauth owns
the popup leg. Each host keeps only what is genuinely its own: the wizard saves a
variable and resource then verifies the connection, the resource drawer fills in its
own form. Both trigger authorization themselves, so a host can offer it a screen
earlier than the step does.
The lists load behind a spinner because which mode to open on depends on whether the
account has projects; deciding that after rendering flipped the toggle under the user.
Adds a kitchen_sink playground for the checklist so the animation and every failure
position can be exercised without a backend, a superadmin, or a Supabase account.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): tidy the resource drawer around the Supabase entry point
Connect Supabase was a hand-styled anchor carrying Supabase's brand hex values
rather than a Button, and it sat in a row whose other controls had settled on
unifiedSize md. Making it a Button meant SupabaseIcon had to satisfy IconType, so it
now takes `size` (deriving height/width from it) alongside the string props its other
callers pass.
The manual resource form spaced every field 32px apart and WhitelistIp added another
16px of its own, which read as a gap rather than a rhythm. One gap of 16px, with the
form itself given a little more separation from the description above it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): stop Supabase resources coming up modified when first opened
Resource forms fill in every unset property from the schema as soon as they render,
so a postgresql resource saved without region, root_certificate_pem and use_iam_auth
was dirty -- and had saved a draft -- the first time anyone looked at it. Write them
with the rest of the value.
SupabaseConnect also rebuilt the resource shape by hand instead of using the shared
helper, which is how the pooler host format ended up in two places.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(backend): record where a data table came from and whether setup finished
edit_datatable_config replaces the whole datatables map and DataTable does not deny
unknown fields, so anything the request omits is dropped without a word. origin and
setup_incomplete would have been erased by any unrelated save;
preserve_unmanaged_datatable_fields carries them -- and migrations_enabled, which had
the same problem inline -- forward for entries that already exist, following renames.
setup_incomplete is what lets a row be recorded before the resource it points at
exists, so the wizard can write nothing until the user finishes. There is deliberately
no intermediate state: the setup runs entirely in the browser, so nothing server-side
could advance one.
datatable_health probes every data table at once for the settings page and skips the
incomplete ones, whose resource_path resolves to nothing yet. set_datatable_setup
patches a single entry instead of resending the map. test_datatable_connection_value
checks a connection the caller has not saved anywhere, which the wizard needs before
it has written a resource.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): make destructive default and subtle buttons read red
Both variants were neutral until the pointer arrived, then filled solid red: nothing
marked the button as destructive until you were already on it. They now carry red text
at rest, with a faded red border on default and a light red wash on hover, which is
what the legacy red border style in the same file had always done.
Three call sites passed color="red" alongside a design-system variant. getStyleClass
returns before colour is read for accent, accent-secondary, default and subtle, so the
delete-migration control, its modal confirm and the import-database button had all been
rendering neutral. They pass destructive now.
The dropdown variant strips the button's own border, and matched border-border-light
literally -- a class the destructive style no longer contains.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(frontend): rebuild data table setup around a read-only row
The wizard gathers intent over two steps, reviews it on a third and writes nothing
until Finish, so a billable Supabase project is created only once the user has seen
what will happen. runSetup is also the retry: every step probes for its own result
before doing anything, so running it again on a half-finished data table resumes
instead of duplicating. Its steps are keyed rather than dispatched on their titles,
where rewording one changed what it did.
The settings row stops being an editable form with a dirty/save cycle. It carries the
name, where the database came from, a health dot and two actions; everything rare
moved into the gear panel, which also offers Finish setup for a data table whose
wizard never completed. Manage is ExploreAssetButton, the control the ducklake list
already uses, and the row and panel both link out to the underlying resource.
supabaseResourceValue no longer assembles the pooler host from the region.
aws-0-<region>.pooler.supabase.com is wrong for any project Supabase allocated
elsewhere, so the host, user and port come from the pooler config endpoint.
Two data tables sharing one database also share _wm_migrations, which is probed
unqualified, so the review step warns when the database being connected is already
behind another data table.
SupabaseConnect is deleted. The resource drawer uses the shared project step
restricted to existing projects: creating one is a billed action and belongs in the
wizard, which has somewhere to report what it did. The kitchen_sink checklist
playground goes with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): fall back to a direct Supabase connection when the pooler cannot be read
Reading a project's Supavisor config needs the database_pooling_config_read scope, which
an instance's Supabase OAuth app may never have been granted. No retry recovers from
that, and the wizard treated it as fatal: the user was left with an error and no way to
finish connecting a project that was otherwise fine.
resolveSupabaseConnection replaces the bare pooler read everywhere it happened. Asking
for session pooling and failing now yields a direct connection plus the reason, which
supabaseResourceValue already knew how to write. Nothing about the fallback is silent --
direct is IPv6-only, which is the whole reason session pooling is the default -- so the
wizard warns on its review step and the resource drawer says so in its toast.
The row is recorded before credentials are saved, so an origin claiming session pooling
has to be corrected once a direct host is what gets written; the run patches it through
set_datatable_setup rather than leaving the panel to report a mode nothing uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(frontend): open the database behind a data table, and say when it cannot write
Every database in the list now opens the surface that owns its credentials. A postgres
one opens its resource in the editor drawer; a Windmill instance one opens the instance
modal, which is where its setup checks, password rotation and drop already lived. Both
are reachable from the row and from the panel's provenance list, and the provider icon
moved inside the button so the whole thing is one target.
CustomInstanceDbWizardModal targeted #content unconditionally, which put it underneath
the panel drawer that now opens it. It takes a target, and the panel portals it to the
body.
The status column gains a third state. The probe reports privileges but nothing gated
the dot on them, so a data table whose role cannot create tables showed as Connected and
only failed when someone ran a migration. It reads "Limited permissions" instead, and
opens the panel on the report carrying the GRANTs that fix it -- the settings page has
already probed, so the panel takes that report rather than asking the user to run Test
connection over work already done. fullyPrivileged is exported from the report component
so the dot and the report cannot disagree about what counts as healthy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* revert(frontend): keep the data tables settings table as it was
The settings table and the setup wizard are two changes that only shared a file. Splitting
them makes each reviewable: this branch keeps the wizard, and the read-only row, gear
panel, health probe and clickable databases move to their own branch.
The rows go back to the editable form with its pickers and save footer, still opening the
wizard from Add a database. DataTableSettingsPanel, dataTableHealth and dataTableOrigin
had no other consumers and go with them; the connection report stays, because the wizard
shows it too.
DataTableSettingsType keeps `origin`: the wizard writes it, and the review step reads it
back to warn when two data tables would share one database and therefore one
_wm_migrations table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): confirm before dismissing the data table wizard mid-setup
Closing was guarded while a run was in flight and unguarded before one, which is backwards:
a run leaves a row to resume from, whereas a backdrop click on the review step threw away
the project, the pasted password and the folder with nothing to recover them from.
Backdrop, Escape and the close button now go through one path that asks first. It only asks
when there is something to lose -- no provider chosen yet, or a run that already produced a
result, closes immediately -- so the dialog does not become something to click through.
Continue in the background still leaves in one click; that exit was always the deliberate
one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): stop the wizard claiming the resource folder controls who can use a data table
"Who can use this database" was wrong. Every path that resolves a datatable:// reference --
both executors and the agent-worker endpoint -- reads the resource unchecked, by workspace
and name. A resource in u/admin is usable by everyone's scripts. The folder governs who can
see and edit the connection, and who can reference the resource directly in a SQL step;
neither is who can use the data table. The wizard was contradicting the tab's own
description two screens later.
The folder select and name field become one Path picker, the same one the resource,
variable and script forms use, so the review step reads as a resource path rather than a
permission choice. Its initialPath is snapshotted when the step opens: Path seeds itself
from it, and a live value fights the typing. Finish now also gates on Path's error, so a
taken or malformed path stops the run before it writes anything.
The button that opens all this says "Add a data table" -- the data table is what you get;
the database is a detail chosen along the way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* revert(frontend): move the destructive button restyle out of the wizard PR
This reverts 3881e4d8ea. Making default and subtle destructive buttons red at rest changes
every existing caller of the prop -- the workspace integrations, AI skills, workspace
creation and the instance database drop -- so it is a design-system change, and the call
sites it fixed are the migrations list and the database manager. None of that is the setup
wizard.
Nothing on this branch passes destructive any more, so it leaves with no loose ends.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): make the wizard stepper navigate the steps it already offers
Stepper dispatches a click and paints cursor-pointer on every reached step, but the wizard
never listened, so the breadcrumbs invited a click and did nothing.
They now reach any step already passed, in either direction: going back to check something
should not cost the progress, which means tracking the furthest step reached rather than
the current one. Forward movement still only happens through the primary action, so a step
is never reachable without having been validated -- and changing the intent revokes the
steps ahead of it, or Finish could run against a review built from something the user has
since edited. The five places that cleared the probe on an edit now do both through one
call.
During a run nothing is reachable, and the stepper says so rather than showing a pointer
over steps that will not respond.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): restore the data tables description lost in the branch split
The rewritten description went into DataTableSettings.svelte shortly before that file was
restored wholesale to its pre-rebuild state, so it left with the row rework it had nothing
to do with. The tab went back to describing the plumbing -- a fully managed PostgreSQL
database, reachable from the SDK -- which never answered the question a new user actually
has: why this rather than a Postgres resource.
It leads with what a data table is, then the two things a resource cannot do -- nobody
needs the credentials to query it, and the name can be pointed at another database without
editing anything that uses it -- and closes with what Windmill runs on top. Both middle
claims are the ones every resolution path backs up: datatable:// resolves by workspace and
name, unchecked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(backend): say what is missing when a $res: or $var: reference does not resolve
Both interpolations fetched with fetch_one and mapped the error through to_anyhow, so a
reference to something deleted surfaced as "no rows returned by a query that expected to
return at least one row @workspaces.rs:2169". It names neither the kind of thing that was
missing nor its path, and it is what a data table pointing at a deleted resource reports.
They now fetch_optional and return NotFound naming the path, and datatable resolution adds
the data table on the way out: the caller asked for one by name, and a bare "resource
f/x/y does not exist" leaves them to work out which of them points at it. The health probe
is new, so this string had only just become something users read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(frontend): gate the data table wizard behind a dev flag
The wizard only appears with `dataTableWizard` set in localStorage; without it the
settings page keeps the inline-row flow it had before this branch, down to the empty-state
copy and the "New Data Table" button, and the wizard component is not mounted at all. The
existing e2e suite drives that button, so the default-off flag is also what keeps it green.
Step 2 of "your own database" becomes one list rather than a segmented control: the
workspace's Postgres resources, then a New resource card that expands in place. A
connection string is not an alternative to a resource, it is how one is written, and the
old layout taught otherwise. The card holds the same connection as a string or as fields
and carries values across when you switch, so `parse` and `compose` have to be inverses --
hence the percent-encoding on both sides, which also fixes a password containing `@`
silently corrupting in the resource form. The Supabase step now uses the same shape.
Names and paths are checked as they are typed rather than at the end of a run that may
have created a billed project first: the data table name against the charset
`edit_datatable_config` enforces, the instance database name against what
`setup_custom_instance_db` will accept, and the resource path against both the resource
and variable namespaces, since the run writes to both and both writes upsert.
`test_datatable_connection_value` refuses `$var:`/`$res:` in its body. It feeds
`transform_json_value_unchecked`, which resolves references with no permission check of its
own, so an admin could otherwise have had the API server decrypt any workspace secret and
hand it to a host the same request chose -- without the audit trail a variable read leaves.
Callers testing something unsaved hold the literal value already.
Alert, SetupChecklist and postgresConnectionString change for everyone, not just behind the
flag: body-only alerts no longer reserve an empty title row, the checklist can nest the
checks a step is made of, and the connection-string parser is shared with the resource form.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: pin ee-repo-ref to the EE branch merged with EE main
The Supabase proxies the wizard calls are still unmerged, so the ref cannot be an EE
main commit yet; it now names that branch merged with EE main rather than the branch
alone, which was nine commits behind and would have been built against a CE main it
never saw.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(frontend): gate the supabase resource path behind the dev flag
* test(frontend): pin connection string parsing to libpq behaviour
* fix(frontend): keep the supabase resource link off the popup callback path
* refactor(frontend): load the supabase resource dialog only behind the flag
* fix(frontend): refuse a resource path the wizard run does not own
* fix(frontend): let a failed data table setup be corrected without losing what it made
* fix(frontend): let a failed setup reuse the resource path it claimed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(backend): record the two data table connection tests in the audit log
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): use Section for the data table wizard advanced group
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): read connection strings the way libpq does
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(backend): pin the ee ref back to a commit this branch can build
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): keep a failed setup's claims across the redirect and rollback
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(backend): probe a data table with the auth mode the worker will use
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): keep every part of a connection string through the round trip
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(frontend): give a setup run one record of what it created
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): mark a resource claim by edited_at, not its creator
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): mark every claim by revision, and keep an unconfirmed project's secret
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): refuse to test or save behind a connection string that will not parse
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): refuse a connection string carrying options the resource cannot hold
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): allowlist the connection-string parameters a resource can honour
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): guard every created Supabase project, not just the last one
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): do not warn about renaming an item that does not exist yet
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): make the review step read as one list of what will exist
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): keep the picked Supabase project across the redirect, reject connect_timeout
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: check the data table connection from a worker, not the API server
The wizard's connection check ran on the API server through two endpoints added
for it. That server is a different machine with a different identity, so the
answer was about the API server rather than about the worker that will run the
queries: a host reachable from one is not necessarily reachable from the other,
and IAM RDS and Azure workload identity authenticate as whichever process opens
the connection.
Run the privilege query as a preview job instead. A job goes through the
worker's Postgres executor, which is where `PgAuthMode::of` already picks the
authentication mode, and it takes either a resource value or a `$res:` path
exactly as a Postgres step does. Postgres composes the suggested GRANT
statements through `format('%I')`, so identifier quoting stays where it is
already implemented.
Removes `test_datatable_resource_connection` and
`test_datatable_connection_value`, and `connect_as_the_worker_would` with them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: fold check_datatable_connection back into its only caller
The helper was split out so the two connection-test endpoints could share a
body. Those endpoints are gone, leaving one caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* revert: keep the data table connection check schema inline
It was lifted into components so three endpoints could share it. Two of those
are gone, so it is back to one user and the extraction changes nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: restore openapi.yaml to the branch point
The previous commit restored main's tip rather than the merge base, which
carried three unrelated main-only changes into this branch: the resource
mcp_tools truncation fields, the execution_mode description, and a version bump.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(frontend): drop four effects from the data table wizard
Each was doing work a derived, a load callback or a real entry point does
better.
- The name conflict is kept with the name it was raised for and derived from
it. As an effect it was correct only because it never read what it wrote:
the pre-flight sets the message and the effect does not re-trigger, so adding
a read would have cleared it the instant it appeared. The message now also
comes back if the taken name is retyped, which is what the server will say.
- The default resource selection is seeded inside the fetcher that loads the
list, where "has the fetch settled" cannot be asked wrong.
- Reset-on-open becomes an exported open(), called by the settings page, so a
fresh run is set up by the act of opening rather than by a flag emulating
mount.
- The OAuth connects and the folder list become resources; supabaseAvailable
and folders are derived from them. defaultFolder takes the list rather than
reading it, so the fetch can seed off its own result.
Leaves the debounced path check, which is async with an out-of-order guard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(frontend): drop three effects from the Supabase branch
- useSupabaseOauth reports success as onAuthed, alongside the failures it
already reported. SupabaseResourceConnect was watching `authed` to find out;
it takes the callback instead, keeping the guard that stops an authorization
started elsewhere on the page from opening its dialog.
- SupabaseProjectStep loads its orgs and projects through a resource keyed on
the token, so the `loaded` latch goes and re-authorizing reloads rather than
keeping the lists from the expired session.
- SetupChecklist records what the user toggled and derives the open state from
it, a failed step defaulting to open. Recording the open state instead needed
an effect to force it, and that effect re-ran on every progress update, so a
description closed while anything was still ticking reopened. A close now
holds for the life of the checklist, including across Try again.
Leaves the message listener, which subscribes to another window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): confine the modal restyle to the wizard, and trim the comments
The wider side padding and lighter dialog heading were changing all 17 Modal2
dialogs to suit this one flow. They move behind an opt-in `formStyling`, taken
by the three dialogs this branch owns; every other Modal2 renders as it did.
Also drops two comments that cited a design approval rather than a constraint,
and shortens the blocks that had grown past the four lines AGENTS.md asks for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): use the accent token for the wizard's links
`text-blue-500` is the marketing blue `#3B82F6`, which brand-guidelines.md
rules out in the app interface.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: point ee-repo-ref at the EE branch head
Picks up EE main, which the branch now needs, and the Supabase proxy auth fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): read sslmode by name, and stop decrypting a secret to date it
- `sslmode` was found by searching the query text, so it also matched inside
another parameter's value: `?application_name=sslmode=disable` passed the
allowlist on the parameter name and then parsed as a request to turn TLS off,
which both the wizard and the resource form saved and probed. Parsed with
`URLSearchParams` by exact name, with a test.
- `secretMark` read the variable with `decryptSecret` defaulted to true, so
every write decrypted a secret nothing reads and recorded the decryption --
including someone else's on the retry about to refuse it. It wants only
`edited_at`, which is returned either way.
- The probe gave up at 15s while the worker allows its Postgres connect 20s, so
a host that accepts the connection and never answers was cancelled and
reported as a missing worker rather than a failed connection.
- The create-mode region and project name did not report an intent change, so
renaming a project after a name collision left the failure naming the old one.
- Two comments described the code as it was before the claim mark became a
revision, and a doc comment outlived the field it documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): read connection parameters the way libpq does
One reader for both the parser and the allowlist, since they disagreed about
what a string says in two ways that both ended in a weaker connection than was
pasted:
- `URLSearchParams.get` takes the first of a repeated parameter and libpq takes
the last, so `?sslmode=disable&sslmode=require` was read as `disable`.
- The allowlist folded the parameter name and the parser did not, so
`?SslMode=verify-full` was refused by neither and honoured by neither, and
saved as the `require` default.
The parked Supabase run is now handed to `open()` rather than read back off the
`resume` prop it was just assigned to, so restoring it does not depend on when
that prop reaches the component.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): keep connection parameter names case-sensitive
libpq does not fold them: `?SslMode=disable` is rejected as an invalid URI
query parameter rather than read as `sslmode`, which a local server confirms.
Folding made Windmill accept and honour a string Postgres itself refuses;
naming the parameter instead tells the user why it cannot be stored.
The last-value-wins rule for a repeated parameter is unchanged, and matches
what the same server does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): seed the Supabase organization from the project it selects
The loader took `orgs[0]` independently of the project it seeded, so an account
whose first project sits outside its first organization had the review step name
an organization the database does not belong to. Picking a project by hand
already derives it; the seeding now does the same.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): let the probe report an empty search_path instead of failing on it
`format('%I', NULL)` raises rather than returning NULL, so a role whose
search_path names no valid schema failed the whole privilege query and was
reported as an unreachable database. That is the one case `fix_search_path`
exists to name, and it never reached the user. Verified against a local server
with `SET search_path = ''`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): say which of the two refusals a connection string hit
Making parameter names case-sensitive gave `unsupportedConnectionParam` two
reasons to refuse, and the single message explained only one. `?SslMode=` was
answered with "Windmill cannot store SslMode on a Postgres resource", which is
false twice over: sslmode is exactly what the resource stores, and the string
asks for nothing because Postgres rejects the URI. It now names the spelling
when the parameter is one we keep, and the storage limit otherwise.
The folder-list guard also still read the `resume` prop that `open(parked)` was
changed to stop trusting, so the resumed path now comes from whatever `reset`
was handed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): leave the Supabase organization unset when the lookup misses
Falling back to the first organization named one the seeded project is not in,
since `supabaseSummary` prefers `intent.org` over the project's own. Unset, it
falls through to the project's organization identifier — the right one, spelled
as a slug rather than a name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(frontend): pin which refusal a connection string gets
The two messages differ in what they ask the user to do, and the condition
choosing between them — whether the lowercased name is one the resource keeps —
is not visible from either call site. `Connect_Timeout` is the case that keeps
them honest: miscased *and* unstorable, so respelling it would not help and the
message must not suggest it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): hand a failed Supabase leg back to the page holding its run
Denial, a token error and a malformed callback all sent the user to
/resources whether or not a run was parked. Nothing else consumes the park, so
the run stayed in sessionStorage and sprang the wizard open on an unrelated
later visit instead. A parked run now lands on the data tables tab, where the
wizard resumes on the setup step and can authorize again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): let a run reuse the name of a row it could not take back out
`removeRow` reports `kept` when the undo cannot reach the server, so the row
this run wrote stays in the workspace config and comes back in `existingNames`.
The client-side name check then refused the retry on the run's own name, with
no way forward but a rename. The instance database name has carried the same
exemption since it was written; this is the data table name catching up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): discard a variable check the wizard has moved on from
The post-await guard compared only the path, and the path is built from the
review step's fields -- so picking an existing resource stops the wizard minting
one without changing it. A check already in flight then answered for a branch
nobody was on, and a `true` disabled Finish over a path the run no longer
writes. The cleanup cannot help: it cancels a pending timer, not a live request.
Both sides of the await now ask the same question.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 483513b70979aa9497cab869837108d948449984
This commit updates the EE repository reference after PR #715 was merged in windmill-ee-private.
Previous ee-repo-ref: 8604b30a740c5620069208801a7ae50937b61977
New ee-repo-ref: 483513b70979aa9497cab869837108d948449984
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Open-source developer platform for internal code: APIs, background jobs, workflows and UIs. Self-hostable alternative to Retool, Pipedream, Superblocks and a simplified Temporal with autogenerated UIs and custom UIs to trigger workflows and scripts as internal apps.
Scripts are turned into sharable UIs automatically, and can be composed together into flows or used into richer apps built with low-code. Supported languages: Python, TypeScript, Go, Bash, SQL, GraphQL, PowerShell, Rust, and more.
Try it - Website - Docs - Discord - Hub - Contributing
Windmill - Developer platform for APIs, background jobs, workflows and UIs
Windmill is fully open-sourced (AGPLv3) and Windmill Labs offers dedicated instances and commercial support and licenses.
https://github.com/user-attachments/assets/d80de1d9-64de-4d89-aacd-6df23fa81fc4
- Windmill - Developer platform for APIs, background jobs, workflows and UIs
Main Concepts
- Define a minimal and generic script in Python, TypeScript, Go or Bash that solves a specific task. The code can be defined in the provided Web IDE or synchronized with your own GitHub repo (e.g. through VS Code extension): provided Web IDE or synchronized with your own GitHub repo (e.g. through VS Code extension):
- Your scripts parameters are automatically parsed and generate a frontend.
- Make it flow! You can chain your scripts or scripts made by the community shared on WindmillHub.
- Build complex UIs on top of your scripts and flows.
Scripts and flows can be triggered by schedules, webhooks, HTTP routes, Kafka, WebSockets, emails, and more.
Build your entire infra on top of Windmill!
Show me some actual script code
//import any dependency from npm
import * as wmill from "windmill-client";
import * as cowsay from "cowsay@1.5.0";
// fill the type, or use the +Resource type to get a type-safe reference to a resource
type Postgresql = {
host: string;
port: number;
user: string;
dbname: string;
sslmode: string;
password: string;
};
export async function main(
a: number,
b: "my" | "enum",
c: Postgresql,
d = "inferred type string from default arg",
e = { nested: "object" }
//f: wmill.Base64
) {
const email = process.env["WM_EMAIL"];
// variables are permissioned and by path
let variable = await wmill.getVariable("f/company-folder/my_secret");
const lastTimeRun = await wmill.getState();
// logs are printed and always inspectable
console.log(cowsay.say({ text: "hello " + email + " " + lastTimeRun }));
await wmill.setState(Date.now());
// return is serialized as JSON
return { foo: d, variable };
}
Local Development
Windmill supports multiple ways to develop locally and sync with your instance:
| Tool | Description |
|---|---|
| CLI | Sync scripts from local files or GitHub, run scripts/flows from the command line |
| VS Code Extension | Edit and test scripts & flows directly from VS Code / Cursor with full IDE support |
| Git Sync | Two-way sync between Windmill and your Git repository |
| Claude Code | AI-assisted development with Claude for scripts, flows, and apps |
https://github.com/user-attachments/assets/c541c326-e9ae-4602-a09a-1989aaded1e9
You can run scripts locally by passing the right environment variables for the wmill client library to fetch resources and variables from your instance. See local development docs.
Stack
- Database: Postgres (compatible with Aurora, Cloud SQL, Neon, Azure PostgreSQL)
- Backend: Rust - stateless API servers and workers pulling jobs from a Postgres queue
- Frontend: Svelte 5
- Sandboxing: nsjail and PID namespace isolation
- Runtimes:
- TypeScript/JavaScript: Bun (default) and Deno
- Python: python3 with uv for dependency management
- Go, Bash, PowerShell, PHP, Rust, C#, Java, Ansible
Fastest Self-Hostable Workflow Engine
We have compared Windmill to other self-hostable workflow engines (Airflow, Prefect & Temporal) and Windmill is the most performant solution for both benchmarks: one flow composed of 40 lightweight tasks & one flow composed of 10 long-running tasks.
All methodology & results on our Benchmarks page.
Security
- Sandboxing: nsjail for filesystem/resource isolation, and PID namespace isolation (enabled by default) to prevent jobs from accessing worker process memory
- Secrets: One encryption key per workspace for credentials stored in Windmill's K/V store. We recommend encrypting the Postgres database as well.
See Security documentation for details.
Performance
Once a job started, there is no overhead compared to running the same script on the node with its corresponding runner (Deno/Go/Python/Bash). The added latency from a job being pulled from the queue, started, and then having its result sent back to the database is ~50ms. A typical lightweight deno job will take around 100ms total.
Architecture
How to self-host
For detailed setup options, see Self-Host documentation.
Docker compose
Deploy Windmill with 3 files (docker-compose.yml, Caddyfile, .env):
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/docker-compose.yml -o docker-compose.yml
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/Caddyfile -o Caddyfile
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/.env -o .env
docker compose up -d
Go to http://localhost - default credentials: admin@windmill.dev / changeme
Using an external database: Set DATABASE_URL in .env to point to your managed Postgres (AWS RDS, GCP Cloud SQL, Azure, Neon, etc.) and set db replicas to 0.
Kubernetes (Helm charts)
helm repo add windmill https://windmill-labs.github.io/windmill-helm-charts/
helm install windmill-chart windmill/windmill --namespace=windmill --create-namespace
See windmill-helm-charts for configuration options.
Cloud providers
Windmill works on AWS (EKS/ECS), GCP, Azure, Ubicloud, Fly.io, Render.com, Hetzner, Digital Ocean, and others. Rule of thumb: 1 worker per 1vCPU and 1-2 GB RAM.
OAuth, SSO & SMTP
Configure OAuth and SSO (Google Workspace, Microsoft/Azure, Okta) directly from the superadmin UI. See documentation.
License
The Community Edition is free to use internally. For commercial redistribution or managed services, contact sales@windmill.dev. See LICENSE and Pricing for details.
The "Community Edition" of Windmill available in the docker images hosted under ghcr.io/windmill-labs/windmill and the github binary releases contains the files under the AGPLv3 and Apache 2 sources but also includes proprietary and non-public code and features which are not open source and under the following terms: Windmill Labs, Inc. grants a right to use all the features of the "Community Edition" for free without restrictions other than the limits and quotas set in the software and a right to distribute the community edition as is but not to sell, resell, serve Windmill as a managed service, modify or wrap under any form without an explicit agreement.
The binary compilable from source code in this repository without the "enterprise" feature flag is open-source under the LICENSE-AGPLv3 License terms and conditions.
To re-expose directly any Windmill parts to your users as a feature of your product, with the exception of iframed public Windmill "apps", or to build a feature on top of "Windmill Community Edition" that you sell commercially or embed in a distributable product or binary, you must get a commercial license. Contact us at sales@windmill.dev if you have any questions. To do the same from the binary compiled from the source code in this repository without the "enterprise" feature flag, you must comply with the AGPLv3 license terms and conditions or get a commercial license from Windmill Labs, Inc.
To use Windmill "Community Edition" as is internally in your organization, or to use its APIs as is, you do NOT need a commercial license.
Integrations
In Windmill, integrations are referred to as resources and resource types. Each Resource has a Resource Type that defines the schema that the resource needs to implement.
On self-hosted instances, you might want to import all the approved resource types from WindmillHub. A setup script will prompt you to have it being synced automatically everyday.
Environment Variables
| Environment Variable name | Default | Description | Api Server/Worker/All |
|---|---|---|---|
| DATABASE_URL | The Postgres database url. | All | |
| WORKER_GROUP | default | The worker group the worker belongs to and get its configuration pulled from | Worker |
| MODE | standalone | The mode if the binary. Possible values: standalone, worker, server, agent | All |
| METRICS_ADDR | None | (ee only) The socket addr at which to expose Prometheus metrics at the /metrics path. Set to "true" to expose it on port 8001 | All |
| JSON_FMT | false | Output the logs in json format instead of logfmt | All |
| BASE_URL | http://localhost:8000 | The base url that is exposed publicly to access your instance. Is overriden by the instance settings if any. | Server |
| ZOMBIE_JOB_TIMEOUT | 30 | The timeout after which a job is considered to be zombie if the worker did not send pings about processing the job (every server check for zombie jobs every 30s) | Server |
| RESTART_ZOMBIE_JOBS | true | If true then a zombie job is restarted (in-place with the same uuid and some logs), if false the zombie job is failed | Server |
| NATIVE_MODE | false | Enable native mode: sets NUM_WORKERS=8, rejects non-native jobs (nativets, postgresql, mysql, etc.) | Worker |
| SLEEP_QUEUE | 50 | The number of ms to sleep in between the last check for new jobs in the DB. It is multiplied by NUM_WORKERS such that in average, for one worker instance, there is one pull every SLEEP_QUEUE ms. | Worker |
| KEEP_JOB_DIR | false | Keep the job directory after the job is done. Useful for debugging. | Worker |
| EXIT_AFTER_N_JOBS | None | Exit the worker process after it has executed that many jobs, so that a supervisor restarts it and no process runs more than that many, bar the steps of a same-worker flow it has started, which it always finishes (set it to 1 for a process per job; jobs handed to a dedicated worker, and the worker's own init and periodic scripts, do not count). Not counting the init and periodic scripts means they run again on every restart: an init script's runtime is added to the latency of every batch of that many jobs, and a periodic script fires once per process start whatever its interval says. The worker's shell in the workers page also starts backed off rather than after the two minutes it otherwise takes, since a process due to be recycled cannot count on living that long: the first command of a session can wait up to 15s, later ones are immediate. For deployments that isolate executions by process lifetime rather than with nsjail; note that a container restart resets the process, not the container filesystem, so caches and /tmp survive it. The worker name is then derived from the hostname instead of being random, so the restarted worker keeps its row in the workers list (an agent worker keeps the row but restarts its job count). Use one worker per process: workers of one process share its environment, so the first to reach the limit shuts the others down too. |
Worker |
| WORKER_SUFFIX | None | Pins the last part of the worker name, which is otherwise random, so that a restarted worker keeps its row in the workers list. Only needed when several worker processes of the same worker group run on one host, since the name is derived from the hostname: give each of them a distinct value, as two processes sharing one must never happen. At most 64 letters, digits and underscores; anything else is refused at startup. | Worker |
| LICENSE_KEY (EE only) | None | License key checked at startup for the Enterprise Edition of Windmill | Worker |
| SLACK_SIGNING_SECRET | None | The signing secret of your Slack app. See Slack documentation | Server |
| COOKIE_DOMAIN | None | The domain of the cookie. If not set, the cookie will be set by the browser based on the full origin | Server |
| DENO_PATH | /usr/bin/deno | The path to the deno binary. | Worker |
| PYTHON_PATH | The path to the python binary if wanting to not have it managed by uv. | Worker | |
| GO_PATH | /usr/bin/go | The path to the go binary. | Worker |
| GOPRIVATE | The GOPRIVATE env variable to use private go modules | Worker | |
| GOPROXY | The GOPROXY env variable to use | Worker | |
| NETRC | The netrc content to use a private go registry | Worker | |
| PY_CONCURRENT_DOWNLOADS | 20 | Sets the maximum number of in-flight concurrent python downloads that windmill will perform at any given time. | Worker |
| PATH | None | The path environment variable, usually inherited | Worker |
| HOME | None | The home directory to use for Go and Bash , usually inherited | Worker |
| DATABASE_CONNECTIONS | 50 (Server)/3 (Worker) | The max number of connections in the database connection pool | All |
| SUPERADMIN_SECRET | None | A token that would let the caller act as a virtual superadmin superadmin@windmill.dev | Server |
| TIMEOUT_WAIT_RESULT | 20 | The number of seconds to wait before timeout on the 'run_wait_result' endpoint | Worker |
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
| DISABLE_RESPONSE_LOGS | false | Disable response logs | Server |
| CREATE_WORKSPACE_REQUIRE_SUPERADMIN | true | If true, only superadmins can create new workspaces | Server |
| MIN_FREE_DISK_SPACE_MB | 15000 | Minimum amount of free space on worker. Sends critical alert if worker has less free space. | Worker |
| RUN_UPDATE_CA_CERTIFICATE_AT_START | false | If true, runs CA certificate update command at startup before other initialization | All |
| RUN_UPDATE_CA_CERTIFICATE_PATH | /usr/sbin/update-ca-certificates | Path to the CA certificate update command/script to run when RUN_UPDATE_CA_CERTIFICATE_AT_START is true | All |
Run a local dev setup
We recommend using Nix. See ./frontend/README_DEV.md for all options.
Frontend only
Uses the backend of https://app.windmill.dev with local frontend (hot-reload):
cd frontend
npm install
npm run generate-backend-client # or generate-backend-client-mac on Mac
npm run dev
Windmill available at http://localhost/
Backend + Frontend
See the ./frontend/README_DEV.md file for all running options.
- Start a local Postgres database using for instance the
start-dev-db.shscript which will make a database available atpostgres://postgres:changeme@localhost:5432/windmillThen run the migrations using the following command:This will also avoid compile time issue with sqlx'scargo install sqlx-cli env DATABASE_URL=<YOUR_DATABASE_URL> sqlx migrate runquery!macro. - (optional, linux only) Install nsjail and have it accessible in your PATH
- Install bun, deno and python3 (+ any languages you want to use), have the bins at
/usr/bin/bun,/usr/bin/deno, and/usr/local/bin/python3or set the corresponding environment variables. - (optional) Install the lld linker
- Go to
frontend/:npm install,npm run generate-backend-clientthenREMOTE=http://localhost:8000 npm run dev- You might need to set some extra heap space for the node runtime
export NODE_OPTIONS="--max-old-space-size=4096" - Create an empty
frontend/buildfolder usingmkdir frontend/build
- Go to
backend/:env DATABASE_URL=<YOUR_DATABASE_URL> RUST_LOG=info cargo run- You can specify any feature flag you want to enable, for example
cargo run --features pythonto enable the python executor.
- Windmill should be available at
http://localhost:3000
Contributing
At this time, we are not seeking outside contribution. Bug reports and feature requests remain very welcome, and small, trivially-verified PRs that fix a problem are still accepted. See CONTRIBUTING.md for the full policy.
Contributors
Copyright
© 2023-2026 Windmill Labs, Inc.






