Skip to content

Cloud-Scale Reference

Complete reference for jac-scale, the cloud-native deployment and scaling plugin for Jac.


pip install jac-scale

jac start app.jac
OptionDescriptionDefault
--portServer port8000
--hostBind address0.0.0.0
--workersNumber of workers1
--reloadHot reload on changesfalse
--scaleDeploy to Kubernetesfalse
--build -bBuild and push Docker image (with —scale)false
--experimental -eInstall from repo instead of PyPI (with —scale)false
--targetDeployment target (kubernetes, aws, gcp)kubernetes
--registryImage registry (dockerhub, ecr, gcr)dockerhub
# Custom port
jac start app.jac --port 3000
# Multiple workers
jac start app.jac --workers 4
# Development with hot reload
jac start app.jac --reload
# Production
jac start app.jac --host 0.0.0.0 --port 8000 --workers 4

Each walker becomes an API endpoint:

walker get_users {
can fetch with Root entry {
report [];
}
}

Becomes: POST /walker/get_users

Walker parameters become request body:

walker search {
has query: str;
has limit: int = 10;
}
curl -X POST http://localhost:8000/walker/search \
-H "Content-Type: application/json" \
-d '{"query": "hello", "limit": 20}'

Walker report values become the response.


The @restspec decorator customizes how walkers and functions are exposed as REST API endpoints.

OptionTypeDefaultDescription
methodHTTPMethodPOSTHTTP method for the endpoint
pathstr"" (auto-generated)Custom URL path for the endpoint
protocolAPIProtocolAPIProtocol.HTTPProtocol for the endpoint (HTTP, WEBHOOK, or WEBSOCKET)
broadcastboolFalseBroadcast responses to all connected WebSocket clients (only valid with WEBSOCKET protocol)

Note: APIProtocol and restspec are builtins and do not require an import statement. HTTPMethod must be imported with import from http { HTTPMethod }.

By default, walkers are exposed as POST endpoints. Use @restspec to change this:

import from http { HTTPMethod }
@restspec(method=HTTPMethod.GET)
walker :pub get_users {
can fetch with Root entry {
report [];
}
}

This walker is now accessible at GET /walker/get_users instead of POST.

Override the auto-generated path:

@restspec(method=HTTPMethod.GET, path="/custom/users")
walker :pub list_users {
can fetch with Root entry {
report [];
}
}

Accessible at GET /custom/users.

@restspec also works on standalone functions:

@restspec(method=HTTPMethod.GET)
def :pub health_check() -> dict {
return {"status": "healthy"};
}
@restspec(method=HTTPMethod.GET, path="/custom/status")
def :pub app_status() -> dict {
return {"status": "running", "version": "1.0.0"};
}

See the Webhooks section below.


curl -X POST http://localhost:8000/user/register \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "secret"}'
curl -X POST http://localhost:8000/user/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "secret"}'

Returns:

{
"access_token": "eyJ...",
"token_type": "bearer"
}
curl -X POST http://localhost:8000/walker/my_walker \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{}'

Configure JWT authentication via environment variables:

VariableDescriptionDefault
JWT_SECRETSecret key for JWT signingsupersecretkey
JWT_ALGORITHMJWT algorithmHS256
JWT_EXP_DELTA_DAYSToken expiration in days7

jac-scale supports SSO with external identity providers. Currently supported: Google.

Configuration:

VariableDescription
SSO_HOSTSSO callback host URL (default: http://localhost:8000/sso)
SSO_GOOGLE_CLIENT_IDGoogle OAuth client ID
SSO_GOOGLE_CLIENT_SECRETGoogle OAuth client secret

SSO Endpoints:

MethodPathDescription
GET/sso/{platform}/loginRedirect to provider login page
GET/sso/{platform}/registerRedirect to provider registration
GET/sso/{platform}/login/callbackOAuth callback handler

Example:

# Redirect user to Google login
curl http://localhost:8000/sso/google/login

LevelValueDescription
NO_ACCESS-1No access to the object
READ0Read-only access
CONNECT1Can traverse edges to/from this object
WRITE2Full read/write access

Use perm_grant to allow all users to access an object at a given level:

with entry {
# Allow everyone to read this node
perm_grant(node, READ);
# Allow everyone to write
perm_grant(node, WRITE);
}

Use allow_root to grant access to a specific user’s root graph:

with entry {
# Allow a specific user to read this node
allow_root(node, target_root_id, READ);
# Allow write access
allow_root(node, target_root_id, WRITE);
}
with entry {
# Revoke all public access
perm_revoke(node);
}
with entry {
# Revoke a specific user's access
disallow_root(node, target_root_id, READ);
}

Walkers have three access levels when served as API endpoints:

AccessDescription
Public (:pub)Accessible without authentication
Protected (default)Requires JWT authentication
Private (:priv)Only accessible by directly defined walkers (not imported)
FunctionSignatureDescription
perm_grantperm_grant(archetype, level)Allow everyone to access at given level
perm_revokeperm_revoke(archetype)Remove all public access
allow_rootallow_root(archetype, root_id, level)Grant access to a specific root
disallow_rootdisallow_root(archetype, root_id, level)Revoke access from a specific root

Webhooks allow external services (payment processors, CI/CD systems, messaging platforms, etc.) to send real-time notifications to your Jac application. Jac-Scale provides:

  • Dedicated /webhook/ endpoints for webhook walkers
  • API key authentication for secure access
  • HMAC-SHA256 signature verification to validate request integrity
  • Automatic endpoint generation based on walker configuration

Webhook configuration is managed via the jac.toml file in your project root.

[plugins.scale.webhook]
secret = "your-webhook-secret-key"
signature_header = "X-Webhook-Signature"
verify_signature = true
api_key_expiry_days = 365
OptionTypeDefaultDescription
secretstring"webhook-secret-key"Secret key for HMAC signature verification. Can also be set via WEBHOOK_SECRET environment variable.
signature_headerstring"X-Webhook-Signature"HTTP header name containing the HMAC signature.
verify_signaturebooleantrueWhether to verify HMAC signatures on incoming requests.
api_key_expiry_daysinteger365Default expiry period for API keys in days. Set to 0 for permanent keys.

Environment Variables:

For production deployments, use environment variables for sensitive values:

export WEBHOOK_SECRET="your-secure-random-secret"

To create a webhook endpoint, use the @restspec(protocol=APIProtocol.WEBHOOK) decorator on your walker definition.

@restspec(protocol=APIProtocol.WEBHOOK)
walker PaymentReceived {
has payment_id: str,
amount: float,
currency: str = 'USD';
can process with Root entry {
# Process the payment notification
report {
"status": "success",
"message": f"Payment {self.payment_id} received",
"amount": self.amount,
"currency": self.currency
};
}
}

This walker will be accessible at POST /webhook/PaymentReceived.

  • Webhook walkers are only accessible via /webhook/{walker_name} endpoints
  • They are not accessible via the standard /walker/{walker_name} endpoint

Webhook endpoints require API key authentication. Users must first create an API key before calling webhook endpoints.

Endpoint: POST /api-key/create

Headers:

  • Authorization: Bearer <jwt_token> (required)

Request Body:

{
"name": "My Webhook Key",
"expiry_days": 30
}

Response:

{
"api_key": "eyJhbGciOiJIUzI1NiIs...",
"api_key_id": "a1b2c3d4e5f6...",
"name": "My Webhook Key",
"created_at": "2024-01-15T10:30:00Z",
"expires_at": "2024-02-14T10:30:00Z"
}

Endpoint: GET /api-key/list

Headers:

  • Authorization: Bearer <jwt_token> (required)

Webhook endpoints require two headers for authentication:

  1. X-API-Key: The API key obtained from /api-key/create
  2. X-Webhook-Signature: HMAC-SHA256 signature of the request body

The signature is computed as: HMAC-SHA256(request_body, api_key)

cURL Example:

API_KEY="eyJhbGciOiJIUzI1NiIs..."
PAYLOAD='{"payment_id":"PAY-12345","amount":99.99,"currency":"USD"}'
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_KEY" | cut -d' ' -f2)
curl -X POST "http://localhost:8000/webhook/PaymentReceived" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "X-Webhook-Signature: $SIGNATURE" \
-d "$PAYLOAD"
FeatureRegular Walker (/walker/)Webhook Walker (/webhook/)
AuthenticationJWT Bearer tokenAPI Key + HMAC Signature
Use CaseUser-facing APIsExternal service callbacks
Access ControlUser-scopedService-scoped
Signature VerificationNoYes (HMAC-SHA256)
Endpoint Path/walker/{name}/webhook/{name}
MethodPathDescription
POST/webhook/{walker_name}Execute webhook walker
MethodPathDescription
POST/api-key/createCreate a new API key
GET/api-key/listList all API keys for user
DELETE/api-key/{api_key_id}Revoke an API key
HeaderRequiredDescription
Content-TypeYesMust be application/json
X-API-KeyYesAPI key from /api-key/create
X-Webhook-SignatureYes*HMAC-SHA256 signature (*if verify_signature is enabled)

Jac Scale provides built-in support for WebSocket endpoints, enabling real-time bidirectional communication between clients and walkers.

WebSockets allow persistent, full-duplex connections between a client and your Jac application. Unlike REST endpoints (single request-response), a WebSocket connection stays open, allowing multiple messages to be exchanged in both directions. Jac Scale provides:

  • Dedicated /ws/ endpoints for WebSocket walkers
  • Persistent connections with a message loop
  • JSON message protocol for sending walker fields and receiving results
  • JWT authentication via query parameter or message payload
  • Connection management with automatic cleanup on disconnect
  • HMR support in dev mode for live reloading

To create a WebSocket endpoint, use the @restspec(protocol=APIProtocol.WEBSOCKET) decorator on an async walker definition.

@restspec(protocol=APIProtocol.WEBSOCKET)
async walker : pub EchoMessage {
has message: str;
has client_id: str = "anonymous";
async can echo with Root entry {
report {
"echo": self.message,
"client_id": self.client_id
};
}
}

This walker will be accessible at ws://localhost:8000/ws/EchoMessage.

To create a private walker that requires JWT authentication, simply remove : pub from the walker definition.

Use broadcast=True to send messages to ALL connected clients of this walker:

@restspec(protocol=APIProtocol.WEBSOCKET, broadcast=True)
async walker : pub ChatRoom {
has message: str;
has sender: str = "anonymous";
async can handle with Root entry {
report {
"type": "message",
"sender": self.sender,
"content": self.message
};
}
}

When a client sends a message, all connected clients receive the response, making it ideal for:

  • Chat rooms
  • Live notifications
  • Real-time collaboration
  • Game state synchronization

To create a private broadcasting walker, remove : pub from the walker definition. Only authenticated users can connect and send messages, and all authenticated users receive broadcasts.

  • WebSocket walkers must be declared as async walker
  • Use : pub for public access (no authentication required) or omit it to require JWT auth
  • Use broadcast=True to send responses to ALL connected clients (only valid with WEBSOCKET protocol)
  • WebSocket walkers are only accessible via ws://host/ws/{walker_name}
  • The connection stays open until the client disconnects

Jac provides a built-in storage abstraction for file and blob operations. The core runtime ships with a local filesystem implementation, and jac-scale can override it with cloud storage backends — all through the same store() builtin.

The recommended way to get a storage instance is the store() builtin. It requires no imports and is automatically hookable by plugins:

# Get a storage instance (no imports needed)
glob storage = store();
# With custom base path
glob storage = store(base_path="./uploads");
# With all options
glob storage = store(base_path="./uploads", create_dirs=True);
ParameterTypeDefaultDescription
base_pathstr"./storage"Root directory for all files
create_dirsboolTrueCreate base directory if it doesn’t exist

Without jac-scale, store() returns a LocalStorage instance. With jac-scale installed, it returns a configuration-driven backend (reading from jac.toml and environment variables).

All storage instances provide these methods:

MethodSignatureDescription
uploadupload(source, destination, metadata=None) -> strUpload a file (from path or file object)
downloaddownload(source, destination=None) -> bytes|NoneDownload a file (returns bytes if no destination)
deletedelete(path) -> boolDelete a file or directory
existsexists(path) -> boolCheck if a path exists
list_fileslist_files(prefix="", recursive=False)List files (yields paths)
get_metadataget_metadata(path) -> dictGet file metadata (size, modified, created, is_dir, name)
copycopy(source, destination) -> boolCopy a file within storage
movemove(source, destination) -> boolMove a file within storage
import from http { UploadFile }
import from uuid { uuid4 }
glob storage = store(base_path="./uploads");
walker :pub upload_file {
has file: UploadFile;
has folder: str = "documents";
can process with Root entry {
unique_name = f"{uuid4()}.dat";
path = f"{self.folder}/{unique_name}";
# Upload file
storage.upload(self.file.file, path);
# Get metadata
metadata = storage.get_metadata(path);
report {
"success": True,
"storage_path": path,
"size": metadata["size"]
};
}
}
walker :pub list_files {
has folder: str = "documents";
has recursive: bool = False;
can process with Root entry {
files = [];
for path in storage.list_files(self.folder, self.recursive) {
metadata = storage.get_metadata(path);
files.append({
"path": path,
"size": metadata["size"],
"name": metadata["name"]
});
}
report {"files": files};
}
}
walker :pub download_file {
has path: str;
can process with Root entry {
if not storage.exists(self.path) {
report {"error": "File not found"};
return;
}
content = storage.download(self.path);
report {"content": content, "size": len(content)};
}
}

Configure storage in jac.toml:

[storage]
storage_type = "local" # Storage backend type
base_path = "./storage" # Base directory for files
create_dirs = true # Auto-create directories
OptionTypeDefaultDescription
storage_typestring"local"Storage backend (local)
base_pathstring"./storage"Base path for file storage
create_dirsbooleantrueAutomatically create directories

Environment Variables:

VariableDescription
JAC_STORAGE_TYPEStorage type (overrides jac.toml)
JAC_STORAGE_PATHBase directory (overrides jac.toml)
JAC_STORAGE_CREATE_DIRSAuto-create directories ("true"/"false")

Configuration priority: jac.toml > environment variables > defaults.

For advanced use cases, you can use StorageFactory directly instead of the store() builtin:

import from jac_scale.factories.storage_factory { StorageFactory }
# Create with explicit type and config
glob config = {"base_path": "./my-files", "create_dirs": True};
glob storage = StorageFactory.create("local", config);
# Create using jac.toml / env var / defaults
glob default_storage = StorageFactory.get_default();

POST /traverse
ParameterTypeDescriptionDefault
sourcestrStarting node/edge IDroot
depthintTraversal depth1
detailedboolInclude archetype contextfalse
node_typeslistFilter by node typesall
edge_typeslistFilter by edge typesall
curl -X POST http://localhost:8000/traverse \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"depth": 3,
"node_types": ["User", "Post"],
"detailed": true
}'

walker async_processor {
has items: list;
async can process with Root entry {
results = [];
for item in self.items {
result = await process_item(item);
results.append(result);
}
report results;
}
}

Direct database operations without graph layer abstraction. Supports MongoDB (document queries) and Redis (key-value with TTL/atomic ops).

import from jac_scale.lib { kvstore }
with entry {
mongo_db = kvstore(db_name='my_app', db_type='mongodb');
redis_db = kvstore(db_name='cache', db_type='redis');
}

Parameters: db_name (str), db_type (‘mongodb’|‘redis’), uri (str|None - priority: explicit → MONGODB_URI/REDIS_URL env vars → jac.toml)


Common Methods: get(), set(), delete(), exists() Query Methods: find_one(), find(), insert_one(), insert_many(), update_one(), update_many(), delete_one(), delete_many(), find_by_id(), update_by_id(), delete_by_id()

Example:

import from jac_scale.lib { kvstore }
with entry {
db = kvstore(db_name='my_app', db_type='mongodb');
db.insert_one('users', {'name': 'Alice', 'role': 'admin', 'age': 30});
alice = db.find_one('users', {'name': 'Alice'});
admins = list(db.find('users', {'role': 'admin'}));
older = list(db.find('users', {'age': {'$gt': 28}}));
db.update_one('users', {'name': 'Alice'}, {'$set': {'age': 31}});
db.delete_one('users', {'name': 'Bob'});
db.set('user:123', {'status': 'active'}, 'sessions');
}

Query Operators: $eq, $gt, $gte, $lt, $lte, $in, $ne, $and, $or


Common Methods: get(), set(), delete(), exists() Redis Methods: set_with_ttl(), expire(), incr(), scan_keys()

Example:

import from jac_scale.lib { kvstore }
with entry {
cache = kvstore(db_name='cache', db_type='redis');
cache.set('session:user123', {'user_id': '123', 'username': 'alice'});
cache.set_with_ttl('temp:token', {'token': 'xyz'}, ttl=60);
cache.set_with_ttl('cache:profile', {'name': 'Alice'}, ttl=3600);
cache.incr('stats:views');
sessions = cache.scan_keys('session:*');
cache.expire('session:user123', 1800);
}

Note: Database-specific methods raise NotImplementedError on wrong database type.


VariableDescriptionDefault
MONGODB_URIMongoDB connection URINone
REDIS_URLRedis connection URLNone
K8s_MONGODBEnable MongoDB deploymentfalse
K8s_REDISEnable Redis deploymentfalse

jac-scale uses a tiered memory system:

TierBackendPurpose
L1In-memoryVolatile runtime state
L2RedisCache layer
L3MongoDBPersistent storage

# Deploy to Kubernetes
jac start app.jac --scale
# Build Docker image and deploy
jac start app.jac --scale --build
jac destroy app.jac
VariableDescriptionDefault
APP_NAMEApplication name for K8s resourcesjaseci
K8s_NAMESPACEKubernetes namespacedefault
K8s_NODE_PORTExternal NodePort30001
K8s_CPU_REQUESTCPU resource requestNone
K8s_CPU_LIMITCPU resource limitNone
K8s_MEMORY_REQUESTMemory resource requestNone
K8s_MEMORY_LIMITMemory resource limitNone
K8s_READINESS_INITIAL_DELAYReadiness probe initial delay (seconds)10
K8s_READINESS_PERIODReadiness probe period (seconds)20
K8s_LIVENESS_INITIAL_DELAYLiveness probe initial delay (seconds)10
K8s_LIVENESS_PERIODLiveness probe period (seconds)20
K8s_LIVENESS_FAILURE_THRESHOLDFailure threshold before restart80
DOCKER_USERNAMEDockerHub usernameNone
DOCKER_PASSWORDDockerHub password/tokenNone

Configure specific package versions for Kubernetes deployments:

[plugins.scale.kubernetes.plugin_versions]
jaclang = "0.1.5" # Specific version
jac_scale = "latest" # Latest from PyPI (default)
jac_client = "0.1.0" # Specific version
jac_byllm = "none" # Skip installation
PackageDescriptionDefault
jaclangCore Jac language packagelatest
jac_scaleScaling pluginlatest
jac_clientClient/frontend supportlatest
jac_byllmLLM integration (use “none” to skip)latest

Create a health walker:

walker health {
can check with Root entry {
report {"status": "healthy"};
}
}

Access at: POST /walker/health

walker ready {
can check with Root entry {
db_ok = check_database();
cache_ok = check_cache();
if db_ok and cache_ok {
report {"status": "ready"};
} else {
report {
"status": "not_ready",
"db": db_ok,
"cache": cache_ok
};
}
}
}

with entry {
# Get all roots in memory/database
roots = allroots();
}
with entry {
# Commit memory to database
commit();
}

CommandDescription
jac start app.jacStart local API server
jac start app.jac --scaleDeploy to Kubernetes
jac start app.jac --scale --buildBuild image and deploy
jac destroy app.jacRemove Kubernetes deployment

When server is running:

  • Swagger UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc
  • OpenAPI JSON: http://localhost:8000/openapi.json

jac-scale provides built-in Prometheus metrics collection for monitoring HTTP requests and walker execution. When enabled, a /metrics endpoint is automatically registered for Prometheus to scrape.

Configure metrics in jac.toml:

[plugins.scale.metrics]
enabled = true # Enable metrics collection and /metrics endpoint
endpoint = "/metrics" # Prometheus scrape endpoint path
namespace = "myapp" # Metrics namespace prefix
walker_metrics = true # Enable per-walker execution timing
histogram_buckets = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 10.0]
OptionTypeDefaultDescription
enabledboolfalseEnable Prometheus metrics collection and /metrics endpoint
endpointstring"/metrics"Path for the Prometheus scrape endpoint
namespacestring"jac_scale"Metrics namespace prefix
walker_metricsboolfalseEnable walker execution timing metrics
histogram_bucketslist[0.005, ..., 10.0]Histogram bucket boundaries in seconds

Note: If namespace is not set, it is derived from the Kubernetes namespace config (sanitized) or defaults to "jac_scale".

MetricTypeLabelsDescription
{namespace}_http_requests_totalCountermethod, path, status_codeTotal HTTP requests processed
{namespace}_http_request_duration_secondsHistogrammethod, pathHTTP request latency in seconds
{namespace}_http_requests_in_progressGaugeConcurrent HTTP requests
{namespace}_walker_duration_secondsHistogramwalker_name, successWalker execution duration (only when walker_metrics=true)
# Scrape metrics
curl http://localhost:8000/metrics

The metrics endpoint is auto-registered as a GET route with OpenAPI tag “Monitoring”. Requests to the metrics endpoint itself are excluded from tracking.


Manage sensitive environment variables securely in Kubernetes deployments using the [plugins.scale.secrets] section.

[plugins.scale.secrets]
OPENAI_API_KEY = "${OPENAI_API_KEY}"
DATABASE_PASSWORD = "${DB_PASS}"
STATIC_VALUE = "hardcoded-value"

Values using ${ENV_VAR} syntax are resolved from the local environment at deploy time. The resolved key-value pairs are created as a proper Kubernetes Secret ({app_name}-secrets) and injected into pods via envFrom.secretRef.

  1. At jac start --scale, environment variable references (${...}) are resolved
  2. A Kubernetes Opaque Secret named {app_name}-secrets is created (or updated if it already exists)
  3. The Secret is attached to the deployment pod spec via envFrom.secretRef
  4. All keys become environment variables inside the container
  5. On jac destroy, the Secret is automatically cleaned up
# jac.toml
[plugins.scale.secrets]
OPENAI_API_KEY = "${OPENAI_API_KEY}"
MONGO_PASSWORD = "${MONGO_PASSWORD}"
JWT_SECRET = "${JWT_SECRET}"
# Set local env vars, then deploy
export OPENAI_API_KEY="sk-..."
export MONGO_PASSWORD="secret123"
export JWT_SECRET="my-jwt-key"
jac start app.jac --scale --build

This eliminates the need for manual kubectl create secret commands after deployment.