Skip to content

Authentication#

Most applications need to identify users and protect their data. Jac has authentication built into the runtime -- there's no need to integrate a third-party auth library or manage session tokens manually. The access modifier system (:pub vs :priv) directly controls which endpoints require authentication and which are open to everyone.

The key concept is per-user data isolation: when a function or walker is marked :priv, each authenticated user gets their own isolated graph root. User A's data is completely invisible to User B, enforced at the runtime level. This means you don't need to write authorization checks or filter queries by user ID -- the isolation is automatic.

Prerequisites


Overview#

Authentication in Jac works across both backend approaches. Use def:priv (private functions) or walker:priv (private walkers) to create per-user endpoints where each user gets their own isolated data graph. The frontend uses built-in runtime functions to handle login/signup/logout.

See it in action

The AI Day Planner tutorial (Part 6) walks through adding authentication to a complete app, including per-user data isolation.

Jac provides built-in authentication with these runtime functions:

Function Description
jacLogin(username, password) Login user, returns bool
jacSignup(username, password) Register user, returns dict with success key
jacLogout() Clear auth token
jacIsLoggedIn() Check if user is authenticated
cl import from "@jac/runtime" { jacLogin, jacSignup, jacLogout, jacIsLoggedIn }

Quick Start: Simple Login#

cl import from "@jac/runtime" { jacLogin, jacSignup, jacLogout, jacIsLoggedIn, useNavigate }

cl {
    def:pub LoginPage() -> JsxElement {
        has username: str = "";
        has password: str = "";
        has error: str = "";
        has loading: bool = False;

        navigate = useNavigate();

        async def handleLogin(e: any) -> None {
            e.preventDefault();
            error = "";

            if not username or not password {
                error = "Please fill in all fields";
                return;
            }

            loading = True;
            success = await jacLogin(username, password);
            loading = False;

            if success {
                navigate("/");
            } else {
                error = "Invalid credentials";
            }
        }

        return <div style={{"maxWidth": "400px", "margin": "0 auto", "padding": "2rem"}}>
            <h2>Login</h2>
            <form onSubmit={handleLogin}>
                {error and <div style={{"color": "red", "marginBottom": "1rem"}}>{error}</div>}

                <input
                    type="text"
                    value={username}
                    onChange={lambda e: any -> None { username = e.target.value; }}
                    placeholder="Username"
                    style={{"width": "100%", "padding": "0.5rem", "marginBottom": "1rem"}}
                />

                <input
                    type="password"
                    value={password}
                    onChange={lambda e: any -> None { password = e.target.value; }}
                    placeholder="Password"
                    style={{"width": "100%", "padding": "0.5rem", "marginBottom": "1rem"}}
                />

                <button
                    type="submit"
                    disabled={loading}
                    style={{"width": "100%", "padding": "0.5rem"}}
                >
                    {loading and "Logging in..." or "Login"}
                </button>
            </form>
        </div>;
    }
}

Built-in Auth Functions#

jacLogin#

Authenticates a user and stores the JWT token.

cl import from "@jac/runtime" { jacLogin }

cl {
    async def handleLogin() -> None {
        # jacLogin returns bool (True = success, False = failure)
        success = await jacLogin(username, password);

        if success {
            # User is now logged in, token stored automatically
            navigate("/dashboard");
        } else {
            error = "Invalid credentials";
        }
    }
}

jacSignup#

Registers a new user account.

cl import from "@jac/runtime" { jacSignup }

cl {
    async def handleSignup() -> None {
        # jacSignup returns dict with success key
        result = await jacSignup(username, password);

        if result["success"] {
            # User registered and logged in
            navigate("/dashboard");
        } else {
            error = result["error"] or "Signup failed";
        }
    }
}

jacLogout#

Clears the authentication token.

cl import from "@jac/runtime" { jacLogout }

cl {
    def handleLogout() -> None {
        jacLogout();
        # User is now logged out
        navigate("/login");
    }
}

jacIsLoggedIn#

Checks if user is currently authenticated.

cl import from "@jac/runtime" { jacIsLoggedIn }

cl {
    def:pub NavBar() -> JsxElement {
        isLoggedIn = jacIsLoggedIn();

        return <nav>
            {isLoggedIn and (
                <button onClick={lambda -> None { handleLogout(); }}>Logout</button>
            ) or (
                <a href="/login">Login</a>
            )}
        </nav>;
    }
}

Complete Auth Example#

cl import from "@jac/runtime" {
    jacLogin,
    jacSignup,
    jacLogout,
    jacIsLoggedIn,
    Router,
    Routes,
    Route,
    Link,
    useNavigate
}

cl {
    # === Login Page ===
    def:pub LoginPage() -> JsxElement {
        has username: str = "";
        has password: str = "";
        has error: str = "";
        has loading: bool = False;

        navigate = useNavigate();

        # Check if already logged in
        can with entry {
            if jacIsLoggedIn() {
                navigate("/");
            }
        }

        async def handleLogin(e: any) -> None {
            e.preventDefault();
            error = "";

            if not username.trim() or not password {
                error = "Please fill in all fields";
                return;
            }

            loading = True;
            success = await jacLogin(username, password);
            loading = False;

            if success {
                navigate("/");
            } else {
                error = "Invalid username or password";
            }
        }

        return <div style={{"maxWidth": "400px", "margin": "2rem auto", "padding": "2rem"}}>
            <h2>Login</h2>
            <form onSubmit={handleLogin}>
                {error and <div style={{"color": "#dc2626", "marginBottom": "1rem"}}>{error}</div>}

                <div style={{"marginBottom": "1rem"}}>
                    <input
                        type="text"
                        value={username}
                        onChange={lambda e: any -> None { username = e.target.value; }}
                        placeholder="Username"
                        style={{"width": "100%", "padding": "0.75rem", "border": "1px solid #ddd", "borderRadius": "4px"}}
                    />
                </div>

                <div style={{"marginBottom": "1rem"}}>
                    <input
                        type="password"
                        value={password}
                        onChange={lambda e: any -> None { password = e.target.value; }}
                        placeholder="Password"
                        style={{"width": "100%", "padding": "0.75rem", "border": "1px solid #ddd", "borderRadius": "4px"}}
                    />
                </div>

                <button
                    type="submit"
                    disabled={loading}
                    style={{
                        "width": "100%",
                        "padding": "0.75rem",
                        "background": "#3b82f6",
                        "color": "white",
                        "border": "none",
                        "borderRadius": "4px",
                        "cursor": "pointer"
                    }}
                >
                    {loading and "Logging in..." or "Login"}
                </button>

                <p style={{"textAlign": "center", "marginTop": "1rem"}}>
                    Need an account? <Link to="/signup">Sign up</Link>
                </p>
            </form>
        </div>;
    }

    # === Signup Page ===
    def:pub SignupPage() -> JsxElement {
        has username: str = "";
        has password: str = "";
        has confirmPassword: str = "";
        has error: str = "";
        has loading: bool = False;

        navigate = useNavigate();

        async def handleSignup(e: any) -> None {
            e.preventDefault();
            error = "";

            if not username.trim() or not password {
                error = "Please fill in all fields";
                return;
            }

            if password != confirmPassword {
                error = "Passwords don't match";
                return;
            }

            if password.length < 6 {
                error = "Password must be at least 6 characters";
                return;
            }

            loading = True;
            result = await jacSignup(username, password);
            loading = False;

            if result["success"] {
                navigate("/");
            } else {
                error = result["error"] or "Signup failed";
            }
        }

        return <div style={{"maxWidth": "400px", "margin": "2rem auto", "padding": "2rem"}}>
            <h2>Create Account</h2>
            <form onSubmit={handleSignup}>
                {error and <div style={{"color": "#dc2626", "marginBottom": "1rem"}}>{error}</div>}

                <div style={{"marginBottom": "1rem"}}>
                    <input
                        type="text"
                        value={username}
                        onChange={lambda e: any -> None { username = e.target.value; }}
                        placeholder="Username"
                        style={{"width": "100%", "padding": "0.75rem", "border": "1px solid #ddd", "borderRadius": "4px"}}
                    />
                </div>

                <div style={{"marginBottom": "1rem"}}>
                    <input
                        type="password"
                        value={password}
                        onChange={lambda e: any -> None { password = e.target.value; }}
                        placeholder="Password"
                        style={{"width": "100%", "padding": "0.75rem", "border": "1px solid #ddd", "borderRadius": "4px"}}
                    />
                </div>

                <div style={{"marginBottom": "1rem"}}>
                    <input
                        type="password"
                        value={confirmPassword}
                        onChange={lambda e: any -> None { confirmPassword = e.target.value; }}
                        placeholder="Confirm Password"
                        style={{"width": "100%", "padding": "0.75rem", "border": "1px solid #ddd", "borderRadius": "4px"}}
                    />
                </div>

                <button
                    type="submit"
                    disabled={loading}
                    style={{
                        "width": "100%",
                        "padding": "0.75rem",
                        "background": "#10b981",
                        "color": "white",
                        "border": "none",
                        "borderRadius": "4px",
                        "cursor": "pointer"
                    }}
                >
                    {loading and "Creating account..." or "Sign Up"}
                </button>

                <p style={{"textAlign": "center", "marginTop": "1rem"}}>
                    Already have an account? <Link to="/login">Login</Link>
                </p>
            </form>
        </div>;
    }

    # === Protected Dashboard ===
    def:pub Dashboard() -> JsxElement {
        navigate = useNavigate();

        # Redirect if not logged in
        can with entry {
            if not jacIsLoggedIn() {
                navigate("/login");
            }
        }

        def handleLogout() -> None {
            jacLogout();
            navigate("/login");
        }

        if not jacIsLoggedIn() {
            return <p>Redirecting...</p>;
        }

        return <div style={{"padding": "2rem"}}>
            <h1>Dashboard</h1>
            <p>Welcome! You are logged in.</p>
            <button
                onClick={lambda -> None { handleLogout(); }}
                style={{
                    "padding": "0.5rem 1rem",
                    "background": "#ef4444",
                    "color": "white",
                    "border": "none",
                    "borderRadius": "4px",
                    "cursor": "pointer"
                }}
            >
                Logout
            </button>
        </div>;
    }

    # === Main App ===
    def:pub app() -> JsxElement {
        return <Router>
            <Routes>
                <Route path="/" element={<Dashboard />} />
                <Route path="/login" element={<LoginPage />} />
                <Route path="/signup" element={<SignupPage />} />
            </Routes>
        </Router>;
    }
}

Using AuthGuard for Protected Routes#

For file-based routing, use the built-in AuthGuard component:

cl import from "@jac/runtime" { AuthGuard, Outlet }

# pages/(auth)/layout.jac - Protects all routes in (auth) group
cl {
    def:pub layout() -> JsxElement {
        return <AuthGuard redirect="/login">
            <Outlet />
        </AuthGuard>;
    }
}

The AuthGuard component:

  • Checks if user is logged in via jacIsLoggedIn()
  • If authenticated: renders child routes via <Outlet />
  • If not authenticated: redirects to the specified path

Custom Auth Context (Advanced)#

For complex apps that need shared auth state across components:

cl import from "@jac/runtime" { jacIsLoggedIn, jacLogin, jacLogout }

cl {
    import from react { createContext, useContext }

    glob AuthContext = createContext(None);

    # Auth Provider component
    def:pub AuthProvider(props: dict) -> JsxElement {
        has user: any = None;
        has loading: bool = True;

        can with entry {
            # Check auth status on mount
            if jacIsLoggedIn() {
                # Optionally fetch user data from backend
                user = {"authenticated": True};
            }
            loading = False;
        }

        async def login(username: str, password: str) -> bool {
            success = await jacLogin(username, password);
            if success {
                user = {"authenticated": True};
            }
            return success;
        }

        def logout() -> None {
            jacLogout();
            user = None;
        }

        value = {
            "user": user,
            "loading": loading,
            "isAuthenticated": user != None,
            "login": login,
            "logout": logout
        };

        return <AuthContext.Provider value={value}>
            {props.children}
        </AuthContext.Provider>;
    }

    def useAuth() -> any {
        return useContext(AuthContext);
    }
}

Key Takeaways#

Function Returns Description
jacLogin(user, pass) bool Login, returns True on success
jacSignup(user, pass) dict Signup, returns {success: bool, ...}
jacLogout() void Clear auth token
jacIsLoggedIn() bool Check auth status
AuthGuard component Protect routes in file-based routing

Next Steps#