Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
c1ce69f
initial commit for no-auth local mode
mnafees Jul 1, 2026
e0eb1c0
no-auth authz
mnafees Jul 1, 2026
31afe32
CLI and docs
mnafees Jul 1, 2026
62f1aaf
fix build error
mnafees Jul 1, 2026
afeb5fb
error boundary
mnafees Jul 1, 2026
9e16f52
documentation changes
mnafees Jul 1, 2026
ad36f41
local keyset
mnafees Jul 2, 2026
b398a8d
Merge branch 'main' into nafees/local-no-auth
mnafees Jul 2, 2026
3160acb
no auth keyset in hatchet lite + docs
mnafees Jul 2, 2026
1b90b11
rm stale file
mnafees Jul 2, 2026
7b91740
edit doc
mnafees Jul 2, 2026
5ee5b2e
no auth user default to seed admin
mnafees Jul 2, 2026
4a3f9cb
fmt docs
mnafees Jul 2, 2026
8190d2c
simple test for noauth tokens
mnafees Jul 2, 2026
1239b41
Merge branch 'main' into nafees/local-no-auth
mnafees Jul 2, 2026
b574633
auth disabled go build tag
mnafees Jul 3, 2026
a2621cf
dev image
mnafees Jul 3, 2026
f7b85f6
rename strings
mnafees Jul 3, 2026
ee09f00
rename files
mnafees Jul 3, 2026
186511d
update docs
mnafees Jul 3, 2026
2801cf2
prose rewording
mnafees Jul 3, 2026
7e70b77
upload authmode disabled keyset
mnafees Jul 3, 2026
6f35c6a
format
mnafees Jul 5, 2026
955ceca
remove code
mnafees Jul 5, 2026
e84bee1
fix precommitlint
mnafees Jul 5, 2026
ddc7655
Merge branch 'main' into nafees/local-no-auth
mnafees Jul 6, 2026
07fd6b9
fix banner
mnafees Jul 6, 2026
89882df
UI changes for no auth mode
mnafees Jul 6, 2026
be1d730
Merge branch 'main' into nafees/local-no-auth
mnafees Jul 6, 2026
e3da4fe
embed the JWT itself for auth disabled mode
mnafees Jul 6, 2026
898e04b
sane expiry
mnafees Jul 6, 2026
172c9df
Merge branch 'main' into nafees/local-no-auth
mnafees Jul 7, 2026
f558c89
PR comments
mnafees Jul 7, 2026
c0ad591
test for noauth
mnafees Jul 7, 2026
f5dfd4e
auth disabled test
mnafees Jul 7, 2026
a36199c
Merge branch 'main' into nafees/local-no-auth
mnafees Jul 7, 2026
afa0ead
PR comments
mnafees Jul 8, 2026
6db028a
Merge branch 'main' into nafees/local-no-auth
mnafees Jul 8, 2026
f3f18cf
ignore noauth keysets and jwt for gitguardian
mnafees Jul 8, 2026
819c13c
Merge branch 'main' into nafees/local-no-auth
mnafees Jul 8, 2026
305d709
resolve PR comments
mnafees Jul 8, 2026
ed7d836
Merge branch 'main' into nafees/local-no-auth
abelanger5 Jul 8, 2026
54b76a4
chore: make sure we are only building stripped go binaries for Docker…
mnafees Jul 9, 2026
ed9477f
Merge branch 'main' into nafees/local-no-auth
mnafees Jul 9, 2026
07c3852
update docs
mnafees Jul 9, 2026
d5c467e
update docs and build dashboard-dev images as well
mnafees Jul 9, 2026
a327291
Merge branch 'main' into nafees/local-no-auth
mnafees Jul 9, 2026
7e954e8
build all 5 images authdisabled in PR CI
mnafees Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api-contracts/openapi/components/schemas/metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ APIMeta:
type: boolean
description: whether or not a Prometheus federation server is configured (SERVER_PROMETHEUS_SERVER_URL) on this instance
example: false
noAuthEnabled:
Comment thread
mnafees marked this conversation as resolved.
Outdated
type: boolean
description: whether or not authentication is disabled (local no-auth mode) on this instance
example: false

APIMetaAuth:
type: object
Expand Down
31 changes: 31 additions & 0 deletions api/v1/server/authn/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ func (a *AuthN) authenticate(c echo.Context, r *middleware.RouteInfo) error {
return a.handleNoAuth(c)
}

if a.config.Auth.NoAuthEnabled {
return a.handleNoAuthBypass(c)
}
Comment thread
mnafees marked this conversation as resolved.

var bearerErr error

if r.Security.BearerAuth() {
Expand Down Expand Up @@ -138,6 +142,33 @@ func (a *AuthN) handleNoAuth(c echo.Context) error {
return nil
}

func (a *AuthN) handleNoAuthBypass(c echo.Context) error {
forbidden := echo.NewHTTPError(http.StatusForbidden, "Please provide valid credentials")

ctx := c.Request().Context()

user, err := a.config.V1.User().GetUserByEmail(ctx, a.config.Auth.NoAuthUserEmail)

if err != nil {
a.l.Error().Ctx(ctx).Err(err).Msg("no-auth mode: could not resolve default user")

return forbidden
}

c.Set("user", user)
c.Set("auth_strategy", "noauth")

ctx = context.WithValue(ctx, analytics.UserIDKey, user.ID)
Comment thread
mnafees marked this conversation as resolved.
Outdated
ctx = context.WithValue(ctx, analytics.SourceKey, analytics.SourceUI)

span := trace.SpanFromContext(ctx)
telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "user.id", Value: user.ID})

c.SetRequest(c.Request().WithContext(ctx))

return nil
}

func (a *AuthN) handleCookieAuth(c echo.Context) error {
forbidden := echo.NewHTTPError(http.StatusForbidden, "Please provide valid credentials")

Expand Down
2 changes: 2 additions & 0 deletions api/v1/server/authz/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ func (a *AuthZ) authorize(c echo.Context, r *middleware.RouteInfo) error {
var err error

switch c.Get("auth_strategy").(string) {
case "noauth":
err = a.validateUserTenantPermissions(c, r)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is interesting...so even in the case of no auth, if the "no-auth" user tries to access a tenant which they're not a member of, it'll fail? that seems like a good default actually, scopes the no-auth to a single tenant and user (not that anyone will/should run this in production, but still)

case "cookie":
err = a.handleCookieAuth(c, r)
case "bearer":
Expand Down
3 changes: 3 additions & 0 deletions api/v1/server/handlers/metadata/get.go
Comment thread
mnafees marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ func (u *MetadataService) MetadataGet(ctx echo.Context, request gen.MetadataGetR

prometheusServerEnabled := u.config.Prometheus.PrometheusServerURL != ""

noAuthEnabled := u.config.Auth.NoAuthEnabled

meta := gen.APIMeta{
Auth: &gen.APIMetaAuth{
Schemes: &authTypes,
Expand All @@ -48,6 +50,7 @@ func (u *MetadataService) MetadataGet(ctx echo.Context, request gen.MetadataGetR
AllowChangePassword: &u.config.Runtime.AllowChangePassword,
ObservabilityEnabled: &observabilityEnabled,
PrometheusServerEnabled: &prometheusServerEnabled,
NoAuthEnabled: &noAuthEnabled,
}

return gen.MetadataGet200JSONResponse(meta), nil
Expand Down
710 changes: 357 additions & 353 deletions api/v1/server/oas/gen/openapi.gen.go

Large diffs are not rendered by default.

43 changes: 29 additions & 14 deletions cmd/hatchet-cli/cli/internal/drivers/docker/hatchet_lite.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type HatchetLiteOpts struct {
overrideGrpcPort int
imageTag string
pullPolicy pullPolicy
noAuthEnabled bool
}

func initDefaultHatchetLiteOpts() *HatchetLiteOpts {
Expand Down Expand Up @@ -122,6 +123,14 @@ func WithPortsCallback(cb func(dashboardPort, grpcPort int)) HatchetLiteOpt {
}
}

// WithNoAuthMode disables authentication on the local server (local development only).
func WithNoAuthMode(enabled bool) HatchetLiteOpt {
return func(o *HatchetLiteOpts) error {
o.noAuthEnabled = enabled
return nil
}
}

// WithOverrideDashboardPort sets the override dashboard port
func WithOverrideDashboardPort(port int) HatchetLiteOpt {
return func(o *HatchetLiteOpts) error {
Expand Down Expand Up @@ -417,21 +426,27 @@ func (d *DockerDriver) startHatchetLiteContainer(ctx context.Context, opts *Hatc
labels["com.docker.compose.depends_on"] = opts.postgresName
labels["com.docker.compose.image"] = imageInspect.ID

env := []string{
"DATABASE_URL=postgresql://hatchet:hatchet@" + opts.postgresName + ":5432/hatchet?sslmode=disable",
"SERVER_AUTH_COOKIE_DOMAIN=localhost",
"SERVER_AUTH_COOKIE_INSECURE=t",
"SERVER_GRPC_BIND_ADDRESS=0.0.0.0",
"SERVER_GRPC_INSECURE=t",
"SERVER_GRPC_BROADCAST_ADDRESS=localhost:" + fmt.Sprintf("%d", grpcPort),
"SERVER_GRPC_PORT=" + fmt.Sprintf("%d", hatchetInternalGrpcPort),
"SERVER_URL=http://localhost:" + fmt.Sprintf("%d", dashboardPort),
"SERVER_AUTH_SET_EMAIL_VERIFIED=t",
"SERVER_DEFAULT_ENGINE_VERSION=V1",
"SERVER_INTERNAL_CLIENT_INTERNAL_GRPC_BROADCAST_ADDRESS=localhost:" + fmt.Sprintf("%d", hatchetInternalGrpcPort),
}

if opts.noAuthEnabled {
env = append(env, "SERVER_AUTH_NO_AUTH_ENABLED=t")
}

containerConfig := &container.Config{
Image: imageName,
Env: []string{
"DATABASE_URL=postgresql://hatchet:hatchet@" + opts.postgresName + ":5432/hatchet?sslmode=disable",
"SERVER_AUTH_COOKIE_DOMAIN=localhost",
"SERVER_AUTH_COOKIE_INSECURE=t",
"SERVER_GRPC_BIND_ADDRESS=0.0.0.0",
"SERVER_GRPC_INSECURE=t",
"SERVER_GRPC_BROADCAST_ADDRESS=localhost:" + fmt.Sprintf("%d", grpcPort),
"SERVER_GRPC_PORT=" + fmt.Sprintf("%d", hatchetInternalGrpcPort),
"SERVER_URL=http://localhost:" + fmt.Sprintf("%d", dashboardPort),
"SERVER_AUTH_SET_EMAIL_VERIFIED=t",
"SERVER_DEFAULT_ENGINE_VERSION=V1",
"SERVER_INTERNAL_CLIENT_INTERNAL_GRPC_BROADCAST_ADDRESS=localhost:" + fmt.Sprintf("%d", hatchetInternalGrpcPort),
},
Image: imageName,
Env: env,
ExposedPorts: exposedPorts,
Labels: labels,
Healthcheck: &container.HealthConfig{
Expand Down
18 changes: 16 additions & 2 deletions cmd/hatchet-cli/cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ var startCmd = &cobra.Command{
hatchet server start --pull-policy never

# Only pull images if they are not already available locally
hatchet server start --pull-policy missing`,
hatchet server start --pull-policy missing

# Start server with authentication disabled (local development only)
hatchet server start --no-auth-mode`,
Run: func(cmd *cobra.Command, args []string) {
// Get flag values
dashboardPort, _ := cmd.Flags().GetInt("dashboard-port")
Expand All @@ -49,9 +52,14 @@ var startCmd = &cobra.Command{
profileName, _ := cmd.Flags().GetString("profile")
tag, _ := cmd.Flags().GetString("tag")
pullPolicy, _ := cmd.Flags().GetString("pull-policy")
noAuthMode, _ := cmd.Flags().GetBool("no-auth-mode")

opts := []docker.HatchetLiteOpt{}

if noAuthMode {
opts = append(opts, docker.WithNoAuthMode(true))
}

if dashboardPort != 0 {
opts = append(opts, docker.WithOverrideDashboardPort(dashboardPort))
}
Expand All @@ -77,8 +85,13 @@ var startCmd = &cobra.Command{
cli.Logger.Fatalf("%v", err)
}

additionalMessage := ""
if noAuthMode {
additionalMessage = "No-auth mode is enabled: authentication is disabled. Use for local development only."
}

// Render styled output
fmt.Println(serverStartedView(result.ProfileName, result.DashboardPort, result.GrpcPort, ""))
fmt.Println(serverStartedView(result.ProfileName, result.DashboardPort, result.GrpcPort, additionalMessage))
},
}

Expand Down Expand Up @@ -206,6 +219,7 @@ func init() {
startCmd.Flags().StringP("profile", "n", "local", "Name for the local profile (default: local)")
startCmd.Flags().StringP("tag", "t", "latest", `Image tag for the hatchet-lite container (e.g. "v0.83.1")`)
startCmd.Flags().String("pull-policy", "always", `Image pull policy: "always", "missing", or "never"`)
startCmd.Flags().Bool("no-auth-mode", false, "Disable authentication for the local server (local development only, never use in production)")

stopCmd.Flags().StringP("project-name", "p", "", "Docker project name for containers (default: hatchet-cli)")
}
22 changes: 19 additions & 3 deletions frontend/app/src/components/layout/app-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ type AppLayoutProps = {
header: ReactNode;
children: ReactNode;
footer?: ReactNode;
/**
* Rendered as a full-width strip above the header on every page (e.g. no-auth mode warning).
*/
banner?: ReactNode;
/**
* When true, the content area becomes the scroll container.
* When false, content is overflow-hidden (useful when a child layout owns scrolling).
Expand All @@ -17,20 +21,32 @@ export function AppLayout({
header,
children,
footer,
banner,
contentScroll = true,
className,
}: AppLayoutProps) {
const hasFooter = Boolean(footer);
const hasBanner = Boolean(banner);
const gridRows = cn(
hasBanner &&
(hasFooter
? 'grid-rows-[auto_64px_minmax(0,1fr)_auto]'
: 'grid-rows-[auto_64px_minmax(0,1fr)]'),
!hasBanner &&
(hasFooter
? 'grid-rows-[64px_minmax(0,1fr)_auto]'
: 'grid-rows-[64px_minmax(0,1fr)]'),
);
return (
<div
className={cn(
'grid h-full w-full min-h-0 min-w-0 overflow-hidden',
hasFooter
? 'grid-rows-[64px_minmax(0,1fr)_auto]'
: 'grid-rows-[64px_minmax(0,1fr)]',
gridRows,
className,
)}
>
{banner}

{header}

<div className="min-h-0 min-w-0 overflow-hidden">
Expand Down
15 changes: 15 additions & 0 deletions frontend/app/src/components/layout/no-auth-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';

export function NoAuthBanner() {
return (
<div
role="alert"
className="flex items-center justify-center gap-2 border-b border-red-300 bg-red-600 px-4 py-1.5 text-center text-sm font-medium text-white dark:border-red-800"
>
<ExclamationTriangleIcon className="h-4 w-4 flex-shrink-0" />
Heads up: local no-auth mode is on, so there's no sign-in and every
request acts as the default admin. Great for local development, but please
don't use it in production.
</div>
);
}
5 changes: 5 additions & 0 deletions frontend/app/src/lib/api/generated/data-contracts.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions frontend/app/src/pages/authenticated.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getCloudMetadataQuery } from '../hooks/use-cloud.ts';
import { NewTenantSaverForm } from '@/components/forms/new-tenant-saver-form';
import { AppLayout } from '@/components/layout/app-layout';
import { NoAuthBanner } from '@/components/layout/no-auth-banner';
import { AddOrgMemberToTenantModal } from '@/components/modals/add-org-member-to-tenant-modal';
import { CreateTenantInviteModal } from '@/components/modals/create-tenant-invite-modal';
import { InviteModal } from '@/components/modals/invite-modal';
Expand Down Expand Up @@ -41,6 +42,7 @@ import { globalEmitter } from '@/lib/global-emitter';
import { useContextFromParent } from '@/lib/outlet';
import { REDIRECT_TARGET_KEY } from '@/lib/redirect';
import { OutletWithContext } from '@/lib/router-helpers';
import useApiMeta from '@/pages/auth/hooks/use-api-meta';
import { useInactivityDetection } from '@/pages/auth/hooks/use-inactivity-detection';
import { PostHogProvider } from '@/providers/posthog';
import { useUserUniverse } from '@/providers/user-universe';
Expand Down Expand Up @@ -81,6 +83,7 @@ export async function loader(_args: { request: Request }) {

function AuthenticatedInner() {
const { tenant, organizationId } = useTenantDetails();
const { meta } = useApiMeta();
const { capture } = useAnalytics();
const {
currentUser,
Expand Down Expand Up @@ -549,6 +552,11 @@ function AuthenticatedInner() {
<PostHogProvider user={currentUser}>
<SupportChat user={currentUser}>
<AppLayout
banner={
meta && 'noAuthEnabled' in meta && meta.noAuthEnabled ? (
<NoAuthBanner />
) : undefined
}
header={
<TopNav
user={currentUser}
Expand Down
3 changes: 2 additions & 1 deletion frontend/docs/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
1 change: 1 addition & 0 deletions frontend/docs/pages/self-hosting/_meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default {
title: "Managing Hatchet",
},
"configuration-options": "Engine Configuration Options",
"local-no-auth": "Local No-Auth Mode",
"using-pgbouncer": "Using PgBouncer",
"prometheus-metrics": {
title: "Prometheus Metrics",
Expand Down
56 changes: 56 additions & 0 deletions frontend/docs/pages/self-hosting/local-no-auth.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Callout } from "nextra/components";

# Local No-Auth Mode

No-auth mode disables authentication on the Hatchet dashboard and REST API so you can run a local instance without signing in or minting a bearer token for every request. It is intended purely for local development.

<Callout type="error" emoji="🚫">
Never enable no-auth mode in production or on any instance reachable by
untrusted clients. It removes all authentication from the API and dashboard.
</Callout>

## What it does

When enabled, no-auth mode resolves every unauthenticated dashboard and REST API request to the seeded admin user (`ADMIN_EMAIL`, default `admin@example.com`). Concretely:

- **Dashboard** loads without a login screen.
- **REST API** calls succeed without a bearer token or session cookie. The tenant is taken from the request URL, and the admin user is authorized against it exactly as a signed-in owner would be.
- A red banner is shown on every dashboard page while it is active.

## What it does **not** change

The gRPC engine is unaffected. Workers and SDK clients still authenticate with an API token as usual, and **the token's tenant is authoritative**. This is deliberate: it means there are no surprises about which tenant a worker runs against.

So the usual multi-tenant flow works normally:

1. Open the dashboard (no login required) and create a tenant.
2. Create an API token in that tenant.
3. Configure your worker or SDK client with that token — its workflows run in that tenant.

## Enabling it

### Hatchet CLI

Pass `--no-auth-mode` to `hatchet server start`:

```sh
hatchet server start --no-auth-mode
```

### Environment variable

Set the following on both the API and engine:

```sh
SERVER_AUTH_NO_AUTH_ENABLED=true
```

This maps to `auth.noAuthEnabled` in the server config file.

## Safety guarantees

No-auth mode is designed so it cannot be accidentally activated on a managed or production instance:

- It defaults to **off** and must be explicitly enabled.
- It is **ignored whenever a custom authenticator is configured** (as it always is on Hatchet Cloud), so it can never take effect there.
- The server logs a prominent warning on startup while it is enabled.
3 changes: 3 additions & 0 deletions pkg/client/rest/gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading