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
- Completed: Backend Integration
- Time: ~30 minutes
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 |
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#
- Routing - Multi-page applications with file-based routing
- Backend Integration - Calling walkers from frontend