Project

Projects: a repo deployed to a URL, with what the running app needs.

repo = noclick.Repo.from_name("booking", create_if_missing=True)
app = noclick.Project.from_name("booking", repo=repo, create_if_missing=True)
app.bind("DB", noclick.Database.from_name("booking-db", create_if_missing=True))
app.secrets.set("MAPS_API_KEY", "...")
deployment = app.deploy().wait()
print(deployment.url)   # https://booking.noclick.site

A deploy of the production branch goes live at app.url; any other branch gets a preview URL of its own. Bindings, secrets and schedules apply from the next deployment.

Inside the running app, noclick.Project.current() is the app itself, signed in by its deployment (no key in the app's code), and the whole client acts as a key of the app's user: app.database("DB") reaches what it's bound to, noclick.Agent.from_name(..., create_if_missing=True) makes what it needs, app.ai and app.agent("SUPPORT") think and talk as the app, and app.verify_request(...) checks a scheduled request. What the app spends counts toward its budget_usd_per_month ($10 unless set) and its user's cap.

class Project

An app: a repo deployed to a URL. Project.from_name("booking", repo=repo, create_if_missing=True) is lazy; app.deploy() builds and ships it.

Attributes
idstr
namestr
urlstr

Its production URL (https://<name>.noclick.site until a domain serves it).

hostnamestr

Its own name under NoClick's user-sites domain (booking.noclick.site), which never changes (NoClick moves only an app it publishes, when it's renamed where it was published from).

repo_idstr | None

None for an app NoClick publishes (published).

publishedbool

NoClick publishes its files itself (an automation's interface): it has no repo, and deploys when it's published again from there.

workflowdict[str, Any] | None

{id, name} of the automation it was published from (None for every other app).

runtimestr

auto (each deployment runs where its commit says: Deployment.runtime), edge (JavaScript frameworks and files) or container (any server, in any language).

frameworkstr | None

On edge: nextjs, remix, astro, vite or static; on container: python, node or docker (the repo's own Dockerfile); under auto either, which names its runtime. Detected at each build when None.

production_branchstr

The branch whose deploys go live (the repo's default).

domain_nameslist[str]

The live hostnames of the domains serving it, oldest first; domains() loads the domains.

deploy_on_pushstr

What a push to its repo deploys by itself: off (only deploy() deploys), production (a push to the production branch deploys it) or all (any other branch's push deploys a preview too).

preview_resourcesstr

branch: each preview branch gets its own copy of every resource its code names, made the first time it writes to one (until then it reads production's), so preview data never reaches production; shared: previews use production's resources. New apps start with branch.

budget_usd_per_monthfloat | None

The most it may spend in a calendar month, in US dollars: $10 unless its maker set another. An app with code always has one; None only for an app NoClick publishes.

check_pathslist[str]

The paths a Deployment.check() loads when it names none, and what a promote checks before production switches to a release (empty: promotes aren't checked).

regionstr | None

The region its server code is placed near (noclick.regions()), fixed when it was created.

user_idstr | None
created_atAny
productionDeployment | None

The deployment serving url (None before the first production deploy).

repoRepo | None

The repo it deploys from (None for an app NoClick publishes).

secretsProjectSecrets

Its secrets: app.secrets.set("MAPS_API_KEY", "...").

settingsProjectSettings

Its plain settings: app.settings.set("PUBLIC_BASE_URL", "...").

emailProjectEmail

Transactional email from the app.

paymentsProjectPayments

Payments into an account the owner claims.

sign_inProjectSignIn

Sign-in for the app's own users.

aiProjectAI

noclick.AI acting as the app, from its own code: app.ai.chat(...).

createclassmethodasync

noclick.Project.create(
    name, *, repo=None, runtime=None, framework=None, production_branch=None,
    budget_usd_per_month=None, region=None, deploy_on_push=None, user=..., client=None
) -> Project

Create a project called name (unique per user; a duplicate raises noclick.Conflict with existing_id) that deploys repo (a noclick.Repo or its id). Without repo a new, empty repo of the same name is created for it. runtime is auto unless given: each deployment runs where its commit says, on the edge (a wrangler config naming its entry, a JavaScript framework, or only files: server code in isolates, static files served from everywhere) or in a container that scales to zero (a Dockerfile, or a server in another language: Flask, Django, Go, or a Node server with a start script), and the deployment says which and why. edge or container pins it there whatever the commit has. framework is detected from the repo at each build when omitted: on edge nextjs, remix, astro, vite or static; on container docker (the repo's own Dockerfile), else python or node. production_branch is the repo's default branch. deploy_on_push (off unless given) makes a push deploy by itself: production deploys a push to the production branch, all a push to any other branch too, as a preview. budget_usd_per_month caps what the app may spend in a calendar month: its hosting and builds, its calls to what it's bound to, its email, its AI and the agent turns it starts, all of which count toward its user's cap too. Every app gets one: $10 unless you pass another (an agent making a site sets one that fits it). At it the app's paid calls stop and its pages keep serving; at twice it its pages pause. region (noclick.regions()) places its server code near that region, for good: put it with the database it binds (region=db.region), so its queries stay short. Without one, your account's default region, else us-east. Its static files are served from everywhere. It says where the code runs, not where data stays: that's the database's and bucket's region.

Parameters
namestrrequired
repoAnydefault None
runtimestr | Nonedefault None
frameworkstr | Nonedefault None
production_branchstr | Nonedefault None
budget_usd_per_monthfloat | Nonedefault None
regionstr | Nonedefault None
deploy_on_pushstr | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Project

The new project, loaded. Nothing is deployed until deploy().

Usage

Deploy code you already committed:

repo = noclick.Repo.from_name("booking")
app = noclick.Project.create("booking", repo=repo)
print(app.url)   # https://booking.noclick.site

Inside as_user the app is that user's, and what it spends counts toward their cap and its own budget:

with noclick.as_user("acme"):
    app = noclick.Project.create("status-page", framework="astro", budget_usd_per_month=10)

from_nameclassmethod

noclick.Project.from_name(
    name, *, repo=None, create_if_missing=False, runtime=None, framework=None,
    production_branch=None, budget_usd_per_month=..., region=None, deploy_on_push=None,
    user=..., client=None
) -> Project

A lazy reference to the project called name in the current (or given) user. With create_if_missing=True your code is the definition: it's created on first use with the fields Project.create takes, and an existing project whose repo, runtime, framework, production branch, budget or deploy_on_push differ is updated to match. A field you leave out is never changed; an app with code always has a budget, so budget_usd_per_month=None raises noclick.InvalidRequest. region is fixed once it's created: an existing app in another region raises noclick.Conflict naming its region.

Parameters
namestrrequired
repoAnydefault None
create_if_missingbooldefault False
runtimestr | Nonedefault None
frameworkstr | Nonedefault None
production_branchstr | Nonedefault None
budget_usd_per_monthAnyomit to leave it unchanged
regionstr | Nonedefault None
deploy_on_pushstr | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Project

A project that loads (or is created, or brought in step with your definition) the first time it's used. Nothing is sent yet.

Usage

Refer to an app by name:

app = noclick.Project.from_name("booking")
print(app.url, app.production)

Or make your code the definition. The same lines set it up the first time and deploy it every time after:

repo = noclick.Repo.from_name("booking", create_if_missing=True)
app = noclick.Project.from_name("booking", repo=repo, budget_usd_per_month=25, create_if_missing=True)
app.deploy().wait()

currentclassmethod

noclick.Project.current(*, client=None) -> Project

The app this code runs in, from inside a deployment. NoClick puts the deployment's own credential in its environment (NOCLICK_DEPLOYMENT_TOKEN), so the app's code holds no key, and the whole client (noclick.Agent, noclick.Database, noclick.AI, ...) acts as a key of the app's user, attributed to the app. Outside a deployment it raises noclick.Unauthenticated; pass client= to act with a key of your own instead. Python runs in an app on the container runtime; an edge app's code uses the TypeScript client's Project.current().

What its code makes (an agent, a database, a sandbox) or buys (a phone number, a domain) is the app's own. Its user's other resources it reads and uses but never changes or deletes (on one bound to the app, it may clear its data), and it never reaches another app, another user, or what another app's code made. It never mints or revokes keys, sets a cap or budget (its own included), changes policy, answers approvals, accepts terms for a person, or reads secrets' values or raw tokens: those raise noclick.PermissionDenied. It talks to agents in threads of its own (agent), and never to one holding the noclick tool. The app's code decides who may make it do what: policy doesn't judge its calls (an agent it messages still runs under its policy).

What the app does is billed to your account, attributed to the app and its user, and counts toward the app's budget_usd_per_month and its user's cap: what its code spends, what it buys (each renewal in the month it renews) and what bills after its request (its numbers' calls and texts, its automations' runs, its MCP servers, channels, machines and cards). A purchase must fit what's left of the budget, and one in a person's name raises noclick.ApprovalRequired until they approve it. Once the budget or the cap is used up the app keeps serving and reading its data, but its paid calls (writes, email, app.ai, agent turns) raise noclick.SpendCapExceeded naming the app; catch it and serve the page without them.

Parameters
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Project

The app, loaded on first use. Nothing is sent yet.

Usage

In a request handler of a deployed app, read what the app is bound to by name:

app = noclick.Project.current()
db = app.database("DB")
rows = db.query("select id, starts_at from bookings where day = ?", ["2026-10-02"])
app.email.send(to="ana@example.com", subject="Your booking", text="See you Friday at 3pm.")

Make what it needs as it goes, which is the app's own from then on:

noclick.Project.current()
helper = noclick.Agent.from_name("helper", instructions="Answer in French.", tools=["search"],
                                 create_if_missing=True)
print(helper.message("Bonjour !", conversation="visitor-91c2").text)

from_idclassmethod

noclick.Project.from_id(project_id, *, client=None, user=...) -> Project

A lazy reference to a project by id.

Parameters
project_idstrrequired
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Project

A project that loads the first time you read a field. Nothing is sent yet.

Usage
app = noclick.Project.from_id("2b7e9c4a-6f1d-4a3b-8e5c-0d9f7a1b3c52")
op = app.deploy()

getclassmethodasync

noclick.Project.get(project_id, *, user=..., client=None) -> Project

Load a project by id now; an unknown id raises noclick.NotFound.

Parameters
project_idstrrequired
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Project

The project, loaded, with its production deployment.

Usage
app = noclick.Project.get("2b7e9c4a-6f1d-4a3b-8e5c-0d9f7a1b3c52")
print(app.name, app.url, app.framework)

listclassmethodasync

noclick.Project.list(
    *, limit=None, user_filter=None, user=..., client=None
) -> list[Project]

The current user's projects (the account's own outside as_user, or those user_filter covers).

Parameters
limitint | None

Return at most this many.

(default None)
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
list[Project]

Every matching project, newest first, fetched page by page, each with its production deployment.

Usage
for app in noclick.Project.list(user_filter=noclick.ALL_USERS):
    live = app.production
    print(app.name, app.url, live.status if live else "not deployed")

list_pageclassmethodasync

noclick.Project.list_page(
    *, cursor=None, limit=None, user_filter=None, user=..., client=None
) -> Page

One page of projects; pass next_cursor back as cursor.

Parameters
cursorstr | None

The next_cursor of the page before; omit it for the first page.

(default None)
limitint | None

Return at most this many.

(default None)
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Page

A page whose data holds the projects and whose next_cursor is None on the last page.

Usage
page = noclick.Project.list_page(limit=50)
while True:
    for app in page.data:
        print(app.name, app.url)
    if page.next_cursor is None:
        break
    page = noclick.Project.list_page(limit=50, cursor=page.next_cursor)

analytics_summaryclassmethodasync

noclick.Project.analytics_summary(
    *, period='7d', tz=None, user_filter=None, user=..., client=None
) -> AnalyticsSummary

Every app's requests over the last period (24h, 7d or 30d) and how many failed, in one read however many apps there are: each production site's, over the same window analytics reads, its days starting in tz (UTC when omitted). Which apps: the current user's, or those user_filter covers, as list. Requests, not visitors.

Parameters
periodstrdefault '7d'
tzstr | Nonedefault None
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
AnalyticsSummary

Its AnalyticsSummary: one line per app, newest first.

Usage

Each app's week, busiest first:

week = noclick.Project.analytics_summary(tz="Europe/Paris")
for line in sorted(week.apps, key=lambda a: a.requests, reverse=True):
    print(line.project_id, line.requests, line.errors)

updateasync

app.update(
    *, repo=..., runtime=..., framework=..., production_branch=...,
    budget_usd_per_month=..., deploy_on_push=..., preview_resources=...,
    check_paths=...
) -> Project

Change the repo it deploys (a noclick.Repo or its id), its runtime (auto, edge or container), its framework, its production_branch, its budget_usd_per_month, what a push deploys (deploy_on_push: off, production or all), the paths a promote checks first (check_paths, up to 10, each starting with /; [] stops checking) or whether previews get their own resources (preview_resources: branch or shared). A new repo, runtime, framework or branch applies from the next deployment (what's live keeps serving where it runs until then), the rest at once. An app with code always has a budget: budget_usd_per_month=None raises noclick.InvalidRequest (only an app NoClick publishes may have none). Its user's cap still applies either way. Pinning a runtime the app's framework doesn't run on raises noclick.InvalidRequest: change the framework with it.

Parameters
repoAnyomit to leave it unchanged
runtimeAnyomit to leave it unchanged
frameworkAnyomit to leave it unchanged
production_branchAnyomit to leave it unchanged
budget_usd_per_monthAnyomit to leave it unchanged
deploy_on_pushAnyomit to leave it unchanged
preview_resourcesAnyomit to leave it unchanged
check_pathsAnyomit to leave it unchanged
Returns
Project

This app, updated. A field you leave out is unchanged.

Usage

Give an app a budget of its own, so it can't spend all of its user's cap:

app = noclick.Project.from_name("booking", user="user_42")
app.update(budget_usd_per_month=25)

Lift it for the rest of the month after a busy launch:

app = noclick.Project.from_name("booking", user="user_42")
if app.usage().limited_by == "project":
    app.update(budget_usd_per_month=50)

Deploy every push to the production branch from now on:

app = noclick.Project.from_name("booking")
app.update(deploy_on_push="production")

Promote only releases whose home and booking pages load cleanly:

app = noclick.Project.from_name("booking")
app.update(check_paths=["/", "/book"])

Give an app made before branched previews its own copies per branch, so a preview's writes never reach production's data:

app = noclick.Project.from_name("booking")
app.update(preview_resources="branch")

Let an app made before auto run each commit where it says (its next deploy of a Dockerfile runs in a container):

app = noclick.Project.from_name("booking")
app.update(runtime="auto", framework=None)
deployment = app.deploy().wait()
print(deployment.runtime, deployment.runtime_reason)

deployasync

app.deploy(*, branch=None, commit=None) -> Operation

Build and deploy branch (the production branch when omitted) at its head, or at commit. A deploy of the production branch goes live at url once it's ready; any other branch is a preview with a URL of its own.

Parameters
branchstr | Nonedefault None
commitstr | Nonedefault None
Returns
Operation

An operation whose .wait() gives the Deployment, ready. A failed build raises noclick.OperationFailed, whose deployment_id names the deployment to read the build log of.

Usage

Deploy production and wait for it:

app = noclick.Project.from_name("booking")
deployment = app.deploy().wait()
print(deployment.url, deployment.commit)

Deploy a branch as a preview, and read the build log when it fails:

app = noclick.Project.from_name("booking")
try:
    preview = app.deploy(branch="new-form").wait()
    print(preview.url)   # https://new-form--booking.noclick.site
except noclick.OperationFailed as failed:
    for line in noclick.Deployment.from_id(failed.deployment_id).logs(kind="build"):
        print(line.message)

deploymentsasync

app.deployments(*, branch=None, limit=20) -> list[Deployment]

Its deployments, newest first, of every branch or of branch.

Parameters
branchstr | Nonedefault None
limitint | None

Return at most this many.

(default 20)
Returns
list[Deployment]

Up to limit deployments (20 by default; None for all), fetched page by page.

Usage
app = noclick.Project.from_name("booking")
for deployment in app.deployments(limit=10):
    print(deployment.branch, deployment.status, deployment.production, deployment.url)

rollbackasync

app.rollback(*, to) -> Operation

Serve an earlier deployment (a Deployment or its id, still ready) at url again, without rebuilding: one routing change, live within a minute. The same as to.promote(). It covers the app's last 10 production deployments; an older one is built again from its commit.

Parameters
toDeployment | strrequired
Returns
Operation

An operation whose .wait() gives that deployment (or, past the window, the one built from its commit), now production.

Usage

Go back to the production deployment before the current one:

app = noclick.Project.from_name("booking")
previous = [d for d in app.deployments(branch=app.production_branch) if d.status == "ready" and not d.production][0]
app.rollback(to=previous).wait()

bindasync

app.bind(name, resource, *, kind=None) -> Binding

Give the app resource under name: a noclick.Database, Bucket, Connection, Dict, Queue, Secret or Agent, or its id with kind (database, bucket, connection, dict, queue, secret or agent). The running app reads it from its environment by that name. Binding a name again replaces what it pointed at; it takes effect at the next deployment. An agent is the app's user's own or one of your account's, and the app talks to it as itself (see agent).

Parameters
namestrrequired
resourceAnyrequired
kindstr | Nonedefault None
Returns
Binding

The binding, naming the resource.

Usage

Give the app its database, its files and the owner's CRM:

app = noclick.Project.from_name("booking")
app.bind("DB", noclick.Database.from_name("booking-db", create_if_missing=True))
app.bind("FILES", noclick.Bucket.from_name("uploads", create_if_missing=True))
app.bind("CRM", noclick.Connection.from_name("hubspot"))
app.deploy()

Give it an agent to answer its visitors:

app = noclick.Project.from_name("booking")
app.bind("SUPPORT", noclick.Agent.from_name("support"))
app.deploy()

With an id, say what it is:

app = noclick.Project.from_name("booking")
app.bind("DB", "5c2e8a1f-3b7d-4e9a-a6c4-8d1f0e2b3a79", kind="database")

bindingsasync

app.bindings() -> list[Binding]

What the app is given, by name.

Returns
list[Binding]

Each binding with the resource it names.

Usage
app = noclick.Project.from_name("booking")
for binding in app.bindings():
    print(binding.name, binding.kind, binding.resource_name)   # DB database booking-db

unbindasync

app.unbind(name) -> None

Take name away from the app at the next deployment. The resource itself is kept.

Parameters
namestrrequired
Usage
app = noclick.Project.from_name("booking")
app.unbind("CRM")

databaseasync

app.database(name) -> Database

The database bound to the app as name, from the app's own code (Project.current()) or yours.

Parameters
namestrrequired
Returns
Database

The noclick.Database; a name bound to something else raises noclick.InvalidRequest, an unbound one noclick.NotFound.

Usage
app = noclick.Project.current()
bookings = app.database("DB").query("select count(*) as n from bookings")
print(bookings.rows[0]["n"])

bucketasync

app.bucket(name) -> Bucket

The bucket bound to the app as name.

Parameters
namestrrequired
Usage
app = noclick.Project.current()
files = app.bucket("FILES")
files.put("invoices/1042.pdf", b"%PDF-...")

dictasync

app.dict(name) -> Dict

The dict bound to the app as name.

Parameters
namestrrequired
Returns
Usage
app = noclick.Project.current()
sessions = app.dict("SESSIONS")
sessions.put("sess_91c2", {"user": "ana"}, ttl_s=3600)

queueasync

app.queue(name) -> Queue

The queue bound to the app as name.

Parameters
namestrrequired
Returns
Usage
app = noclick.Project.current()
app.queue("JOBS").put({"booking": 1042, "send": "reminder"})

connectionasync

app.connection(name) -> Connection

The connection bound to the app as name: the app runs its operations as the account that connected it.

Parameters
namestrrequired
Usage
app = noclick.Project.current()
crm = app.connection("CRM")
crm.run("create_contact", {"email": "ana@example.com"})

agentasync

app.agent(name) -> Agent

The agent bound to the app as name. From the app's own code it acts as the app: message, send and ask start turns in the app's own conversations, and history and conversations read only those. A key the app passes (its visitor's id, say) always names a thread of the app's: visitor-91c2 from the app is app:<app id>:visitor-91c2 to your own code, which reads those threads, and no other app reaches them. The agent's policy still judges its tool calls, and its turns are billed like any turn, attributed to the app: to the agent's budget, the app's and its user's cap. Anything else (update, delete, its triggers, memory, wake-ups or children) raises noclick.PermissionDenied from the app, unless its code made the agent. A preview's code talks to it in threads of the preview's own (app-preview:<app id>:<key>), its tools answering from a simulated world; no app's code talks to an agent that holds the noclick tool.

Parameters
namestrrequired
Returns
Usage

Answer a visitor in a thread of their own, and degrade once the money runs out:

app = noclick.Project.current()
support = app.agent("SUPPORT")
try:
    reply = support.message("Can I move my booking to Saturday?", conversation="visitor-91c2")
    print(reply.text)
except (noclick.SpendCapExceeded, noclick.PaymentRequired):
    print("Chat is resting for now. Email help@acmebooking.com and we'll answer today.")

verify_request

app.verify_request(body, headers, *, tolerance_s=300) -> None

Check that a request to the app came from one of its schedules: body is the raw body exactly as it arrived, headers its headers. Every scheduled request carries a NoClick-Signature header (t=<unix seconds>,v1=<hex>, HMAC-SHA256 of "<t>.<body>") keyed with the app's signing secret, which NoClick sets in the deployment's environment as NOCLICK_SIGNING_SECRET. It sends nothing.

Raises noclick.WebhookSignatureError when the header is missing or malformed, the timestamp is more than tolerance_s from now, or the signature doesn't match; answer such a request 401.

Parameters
bodybytes | strrequired
headersMapping[str, Any]required
tolerance_sintdefault 300
Usage

In a Flask route a schedule calls, in an app on the container runtime:

from flask import Flask, request

server = Flask(__name__)

@server.post("/jobs/reminders")
def reminders():
    app = noclick.Project.current()
    try:
        app.verify_request(request.get_data(), request.headers)
    except noclick.WebhookSignatureError:
        return "", 401
    return "", 204

scheduleasync

app.schedule(cron, *, path, name=None) -> Schedule

POST path on the app on every tick of cron (a noclick.Cron). A schedule is keyed by name (the path when omitted), so calling this again with the same one updates it rather than adding another. Each request carries a signed NoClick-Signature header, so the app can refuse anyone else.

Parameters
cronCronrequired
pathstrrequired
namestr | Nonedefault None
Returns
Schedule

The schedule, active, with its next_run.

Usage

Send reminders at 9am New York time on weekdays:

app = noclick.Project.from_name("booking")
reminders = app.schedule(noclick.Cron("0 9 * * 1-5", tz="America/New_York"), path="/jobs/reminders")
print(reminders.next_run)

Two schedules on one path need names:

app = noclick.Project.from_name("booking")
app.schedule(noclick.Cron("0 * * * *"), path="/jobs/sync", name="hourly-sync")
app.schedule(noclick.Cron("0 3 * * 0"), path="/jobs/sync", name="weekly-full-sync")

schedulesasync

app.schedules() -> list[Schedule]

Its schedules.

Returns
list[Schedule]

Each schedule with its status, next_run and how its last_run went.

Usage
app = noclick.Project.from_name("booking")
for schedule in app.schedules():
    print(schedule.cron, schedule.tz, schedule.path, schedule.status, schedule.last_status)

add_domainasync

app.add_domain(name) -> Domain

Serve the app at the domain name too. A domain you bought through NoClick is pointed at it at once, at its name and www; one owned elsewhere is connected and waits in pending_dns until its records are set (records lists them). Each name is live once its certificate is issued.

A subdomain of a domain your account holds (ana.acmebooking.com under the account's acmebooking.com) can serve any project, a user's included, so each user's app gets an address of yours; only a key acting for the whole account (not a user session or a key pinned to a user) may hand one out. Under a bought domain the record is written for you; under a connected one connected with wildcard=True it needs no record of its own and is live within minutes, and otherwise records lists the one to add.

Parameters
namestrrequired
Returns
Domain

The noclick.Domain, with its status.

Usage

Serve the app at a domain you own:

app = noclick.Project.from_name("booking")
domain = app.add_domain("book.acme.com")
print(domain.status)   # pending_dns until the records are set
for record in domain.records:
    print(record.type, record.name, record.value)

Give a user's app a subdomain of your account's domain:

ana_app = noclick.Project.from_name("store", user="user_42")
domain = ana_app.add_domain("ana.acmebooking.com")
print(domain.status)   # active: the account bought acmebooking.com through NoClick

domainsasync

app.domains() -> list[Domain]

The domains that serve the app.

Returns
list[Domain]

Each noclick.Domain with its status.

Usage
app = noclick.Project.from_name("booking")
for domain in app.domains():
    print(domain.name, domain.status)

remove_domainasync

app.remove_domain(name) -> None

Stop serving the app at name (a bare domain's www too). The domain itself is kept: a bought one stays yours, and one connected for the app stays connected until you delete it. A name that doesn't serve the app raises noclick.NotFound.

Parameters
namestrrequired
Usage
app = noclick.Project.from_name("booking")
app.remove_domain("book.acme.com")

logsasync

app.logs(*, since=None, level=None, deployment=None, limit=100) -> list[LogLine]

What the app printed while serving, oldest first: the last limit lines (at most 1,000), or with since (a timezone-aware datetime) the first limit after it. level keeps warn or error lines only; deployment one deployment's. Lines are kept 7 days.

Parameters
sincestr | datetime | Nonedefault None
levelstr | Nonedefault None
deploymentDeployment | str | Nonedefault None
limitint

Return at most this many.

(default 100)
Returns
list[LogLine]

The lines. Pass the last one's at as since to read on from there.

Usage

The errors of the last hour:

from datetime import datetime, timedelta, timezone

app = noclick.Project.from_name("booking")
an_hour_ago = datetime.now(timezone.utc) - timedelta(hours=1)
for line in app.logs(since=an_hour_ago, level="error"):
    print(line.at, line.message)

Follow the log, a page at a time:

app = noclick.Project.from_name("booking")
lines = app.logs(limit=50)
while lines:
    for line in lines:
        print(line.message)
    lines = app.logs(since=lines[-1].at, limit=50)

analyticsasync

app.analytics(*, period='7d', environment='production', tz=None) -> ProjectAnalytics

How the app's traffic went over the last period (24h, 7d or 30d): its requests each hour (24h) or day, starting in tz (an IANA time zone; UTC when omitted), how many failed, the totals against the same length of time before, by status, how long answers took to begin and the busiest paths. environment: its production site (production), its previews (preview) or both (all).

It counts every request the app's sites answered, its static files and API calls included, people's and bots' alike: requests, not visitors.

Parameters
periodstrdefault '7d'
environmentstrdefault 'production'
tzstr | Nonedefault None
Returns
ProjectAnalytics

Its ProjectAnalytics, read from NoClick's record of each request.

Usage

How the site is doing this week, against last week:

app = noclick.Project.from_name("booking")
week = app.analytics(tz="Europe/Paris")
print(week.requests, week.previous_requests, week.error_rate, week.p95_ms)
for day in week.series:
    print(day.at.date(), day.requests, day.errors)
for page in week.top_paths:
    print(page.path, page.requests)

The last 24 hours, hour by hour:

for hour in app.analytics(period="24h").series:
    print(hour.at, hour.requests)

usageasync

app.usage() -> ProjectUsage

What the app has spent this month, against its budget: everything it cost in usd (hosting and builds, its calls to what it's bound to, its email, its AI and its agent turns), split by_type, and limited_by, what refuses its paid calls now.

Returns
ProjectUsage

Requests served, compute in cpu_ms, usd and its budget_usd, what refuses its paid calls (limited_by), and the period_start it counts from.

Usage

Show the app's spend against its budget:

app = noclick.Project.from_name("booking")
used = app.usage()
print(used.requests, used.cpu_ms, f"${used.usd:.2f}")
if used.budget_usd is not None:
    print(f"${used.usd:.2f} of ${used.budget_usd:.2f}", used.by_type)

Find out why its paid calls fail:

app = noclick.Project.from_name("booking")
if app.usage().limited_by == "user":
    print("Its user reached their monthly cap.")

exportasync

app.export() -> Operation

Package everything the app is, to run it elsewhere: its code as a git bundle, apps/<name>.json with its settings, bindings and secret names (never values), and each database, bucket, dict and queue it binds, each in its own export's format. To put the code on GitHub, use the repo's export_to_github.

Returns
Operation

An operation whose .wait() gives an noclick.ExportBundle.

Usage
app = noclick.Project.from_name("booking")
bundle = app.export().wait()
print(bundle.url)
for item in bundle.contents:
    print(item.kind, item.name, item.detail)

deleteasync

app.delete(*, delete_data=False) -> TrashItem

Delete the app: it goes offline within seconds and to the trash, restorable for 30 days with everything it had (its releases, settings, secrets, bindings, schedules and domains), served again with no build. Its name is free meanwhile. The repo and every bound database and bucket are kept, unless delete_data=True moves the repo to the trash too, with each database and bucket no other app binds.

Parameters
delete_databooldefault False
Returns
TrashItem

Its TrashItem: restore() serves it again where it was.

Usage

Delete the app and keep its data:

app = noclick.Project.from_name("status-page", user="acme")
item = app.delete()

Or delete what only it used as well, then change your mind:

item = noclick.Project.from_name("scratch").delete(delete_data=True)
item.restore()

Deployment

class Deployment

One build of a project at one commit. status goes queued, building, then ready (serving at url), failed (error says why) or canceled. production is True for the one serving the project's own URL.

Attributes
idstr
project_idstr
branchstr
commitstr | None

The sha it was built from.

commit_messagestr | None
statusstr
urlstr | None

Where this deployment serves (a preview URL unless it's production).

productionbool
environmentstr

production for a deploy of the production branch, preview for any other: whose secrets it was built with. A promoted preview keeps preview and serves with production's secrets.

started_bystr

deploy (a deploy(), or promoting a retired one) or push (the app deploys on push, see Project.deploy_on_push).

errorstr | None

Why the build failed, or why a push couldn't deploy, in one line (None unless failed).

runtimestr | None

Where it runs: edge or container, chosen when its build starts (None until then).

runtime_reasonstr | None

Why it runs there, in words: the app's pinned runtime, or what its commit has (a Dockerfile describes its server).

created_atAny
ready_atAny
last_checkdict[str, Any] | None

Its latest check, in short: {operation_id, passed, url, checked_at, paths: [{path, ok, status, problem}]} (None before its first). The check's operation has the whole of it.

from_idclassmethod

noclick.Deployment.from_id(deployment_id, *, client=None, user=...) -> Deployment

A lazy reference to a deployment by id.

Parameters
deployment_idstrrequired
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Deployment

A deployment that loads the first time you read a field. Nothing is sent yet.

Usage
deployment = noclick.Deployment.from_id("0d6c2f8e-4b1a-4e97-a3c5-7f8e9d0c1b2a")
for line in deployment.logs(kind="build"):
    print(line.message)

getclassmethodasync

noclick.Deployment.get(deployment_id, *, user=..., client=None) -> Deployment

Load a deployment by id now; an unknown id raises noclick.NotFound.

Parameters
deployment_idstrrequired
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Deployment

The deployment, loaded.

Usage
deployment = noclick.Deployment.get("0d6c2f8e-4b1a-4e97-a3c5-7f8e9d0c1b2a")
print(deployment.status, deployment.url, deployment.error)

logsasync

deployment.logs(*, kind='build', limit=None) -> list[LogLine]

Its build output (kind="build") or what it printed while serving (kind="runtime"), oldest first.

Parameters
kindstrdefault 'build'
limitint | None

Return at most this many.

(default None)
Returns
list[LogLine]

The lines, or the last limit of them. A build keeps every line; runtime lines are kept 7 days.

Usage

Find out why a build failed:

deployment = noclick.Deployment.get("0d6c2f8e-4b1a-4e97-a3c5-7f8e9d0c1b2a")
if deployment.status == "failed":
    for line in deployment.logs(kind="build"):
        print(line.at, line.level, line.message)

Read the last few lines a preview printed:

preview = noclick.Deployment.from_id("0d6c2f8e-4b1a-4e97-a3c5-7f8e9d0c1b2a")
tail = preview.logs(kind="runtime", limit=20)

cancelasync

deployment.cancel() -> Deployment

Stop a deployment that's still queued or building; one that already finished is left as it is.

Returns
Deployment

This deployment, canceled unless it had already finished.

Usage
deployment = noclick.Deployment.get("0d6c2f8e-4b1a-4e97-a3c5-7f8e9d0c1b2a")
deployment.cancel()
print(deployment.status)   # canceled

checkasync

deployment.check(paths=None) -> Operation

Load paths of this ready deployment where it serves (its preview URL, or the app's URL when it's production) in a browser, one after another: each one's HTTP status, load time, console errors and a screenshot, kept as a file the chat shows. Without paths, the app's check_paths (else its home page). A deployment no URL serves now can't be checked (noclick.Conflict). The browser is billed at its provider's price, by the second.

Parameters
pathsSequence[str] | Nonedefault None
Returns
Operation

An operation whose .wait() gives a DeploymentCheck; passed is False when a path answered 400 or more, didn't load, or a script on it threw.

Usage

Check the pages you changed before you say they work:

preview = noclick.Project.from_name("booking").deploy(branch="new-form").wait()
checked = preview.check(["/", "/book"]).wait()
for page in checked.paths:
    print(page.path, page.status, page.problem, page.screenshot["url"])
    for error in page.console_errors:
        print("  ", error["kind"], error["message"])

requestsasync

deployment.requests(*, since=None) -> RequestOutcomes

How the requests this deployment served went since since (a timezone-aware datetime; the last 24 hours when omitted, at most 7 days back): how many, by status, the error rate and the slowest routes.

Parameters
sincestr | datetime | Nonedefault None
Returns
RequestOutcomes

Its RequestOutcomes, read from NoClick's record of each request it answered, files included.

Usage

See whether a release is failing in production:

app = noclick.Project.from_name("booking")
outcomes = app.production.requests()
print(outcomes.requests, outcomes.error_rate, outcomes.by_status)
for route in outcomes.slowest_routes:
    print(route.method, route.path, route.p95_ms)

promoteasync

deployment.promote(*, force=False) -> Operation

Make this ready deployment the project's production one: its URL starts serving this build within a minute. Promoting an older production deployment is a rollback; a preview is released again with production's secrets, without a build.

An app with check_paths checks the deployment first (check()) and doesn't promote one that fails: the operation fails with the check in its error, and production keeps serving. force=True promotes it without the check. Before production switches, the databases the app binds are bookmarked: undoing the promote (Operation.undo()) puts them back as they were, with the release before.

Instant rollback covers the app's last 10 production deployments, and each branch's newest preview for 30 days. One older than that was retired: promoting it builds its commit again, and the operation (project.deploy) gives the new deployment.

Parameters
forcebooldefault False
Returns
Operation

An operation whose .wait() gives this deployment (or, past the window, the one built from its commit), now production.

Usage

Put a preview live once you've checked it:

app = noclick.Project.from_name("booking")
preview = app.deploy(branch="new-form").wait()
print(preview.url)   # check it here first
preview.promote().wait()

Take it back, data included, if it goes wrong:

promoted = preview.promote()
promoted.wait()
promoted.undo()

Schedule

class Schedule

A project's cron job: NoClick POSTs path on the app on every tick of cron (read in tz). When 5 runs in a row fail the same way over at least 30 minutes it pauses itself (status paused, paused_reason says why) until you resume() it.

Attributes
idstr
project_idstr
namestr

What keys it within the project (the path unless you named it).

cronstr

The five-field cron expression.

tzstr
pathstr
statusstr

active or paused.

paused_reasonstr | None
next_runAny
last_runAny
last_statusstr | None

How the last run went: succeeded (a 2xx answer) or failed (None before the first).

last_errorstr | None

Why the last run failed, in one line (Answered 500.); None after a success.

created_atAny

from_idclassmethod

noclick.Schedule.from_id(schedule_id, *, client=None, user=...) -> Schedule

A lazy reference to a schedule by id.

Parameters
schedule_idstrrequired
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Schedule

A schedule that loads the first time you read a field. Nothing is sent yet.

Usage
schedule = noclick.Schedule.from_id("6a1f3c9e-2d4b-4e8a-b7c0-5d9e8f1a2b3c")
schedule.pause()

getclassmethodasync

noclick.Schedule.get(schedule_id, *, user=..., client=None) -> Schedule

Load a schedule by id now; an unknown id raises noclick.NotFound.

Parameters
schedule_idstrrequired
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Schedule

The schedule, loaded.

Usage
schedule = noclick.Schedule.get("6a1f3c9e-2d4b-4e8a-b7c0-5d9e8f1a2b3c")
print(schedule.cron, schedule.tz, schedule.next_run, schedule.last_status)

pauseasync

schedule.pause() -> Schedule

Stop firing until resume().

Returns
Schedule

This schedule, paused.

Usage
schedule = noclick.Schedule.get("6a1f3c9e-2d4b-4e8a-b7c0-5d9e8f1a2b3c")
schedule.pause()

resumeasync

schedule.resume() -> Schedule

Fire again from the next tick, clearing paused_reason.

Returns
Schedule

This schedule, active, with its next_run.

Usage

Fix what made it fail, then resume it:

app = noclick.Project.from_name("booking")
for schedule in app.schedules():
    if schedule.status == "paused":
        print(schedule.path, schedule.paused_reason)
        schedule.resume()

run_nowasync

schedule.run_now() -> Schedule

Call its path once now, outside the cron, and wait for the answer (at most 30 seconds). A paused schedule runs too, and stays paused.

Returns
Schedule

This schedule, with last_run and last_status from that call.

Usage
schedule = noclick.Schedule.get("6a1f3c9e-2d4b-4e8a-b7c0-5d9e8f1a2b3c")
schedule.run_now()
print(schedule.last_status)   # succeeded

deleteasync

schedule.delete() -> None

Delete the schedule; it stops firing.

Usage
schedule = noclick.Schedule.from_id("6a1f3c9e-2d4b-4e8a-b7c0-5d9e8f1a2b3c")
schedule.delete()

ProjectSecrets

class ProjectSecrets

app.secrets: values the running app reads from its environment by name, set for production deployments, previews or both. Values are write-only; nothing reads one back. An app's environment holds 128 values in all, NoClick's own and each bound secret's included.

setasync

app.secrets.set(name, value, *, environment='all') -> None

Set the secret name (letters, digits and _, starting with a letter) to value (at most 5 KB), replacing any value it had, for environment: production (deploys of the production branch, and what's promoted), preview (every other branch) or all. It reaches the app at the next deployment. Keep a larger value in a bound noclick.Secret or bucket the app reads.

Parameters
namestrrequired
valuestrrequired
environmentstrdefault 'all'
Usage

The same key everywhere:

app = noclick.Project.from_name("booking")
app.secrets.set("MAPS_API_KEY", "...")
app.deploy()

A live key in production and a test key in previews:

app = noclick.Project.from_name("booking")
app.secrets.set("STRIPE_KEY", "sk_live_...", environment="production")
app.secrets.set("STRIPE_KEY", "sk_test_...", environment="preview")

listasync

app.secrets.list(*, environment='all') -> dict[str, datetime]

The project's secrets by name, never their values: those set for environment (production, preview or all).

Parameters
environmentstrdefault 'all'
Returns
dict[str, datetime]

Each secret's name mapped to when its value was last set.

Usage
app = noclick.Project.from_name("booking")
for name, updated_at in app.secrets.list(environment="preview").items():
    print(name, updated_at)

deleteasync

app.secrets.delete(name, *, environment='all') -> None

Delete the secret name from environment (all by default); the app stops seeing it at the next deployment. A name that isn't set there raises noclick.NotFound.

Parameters
namestrrequired
environmentstrdefault 'all'
Usage
app = noclick.Project.from_name("booking")
app.secrets.delete("MAPS_API_KEY")

versionsasync

app.secrets.versions(name, *, environment='all') -> list[Version]

What name held before each change or deletion of the last 30 days, newest first, in environment (production, preview or all), never the values.

Parameters
namestrrequired
environmentstrdefault 'all'
Returns
list[Version]

Each earlier version, with its environment.

Usage
app = noclick.Project.from_name("booking")
for version in app.secrets.versions("STRIPE_KEY", environment="production"):
    print(version.op, version.replaced_at)

restoreasync

app.secrets.restore(name, version) -> None

Set name back to version's value in its environment (deleted since, it's back). It reaches the app at the next deployment.

Parameters
namestrrequired
versionAnyrequired
Usage

Undo deleting a secret:

app = noclick.Project.from_name("booking")
app.secrets.delete("MAPS_API_KEY")
app.secrets.restore("MAPS_API_KEY", app.secrets.versions("MAPS_API_KEY")[0])

ProjectSettings

class ProjectSettings

app.settings: plain values the running app reads from its environment by name, such as a feature flag or a public URL. Unlike app.secrets they read back, and they show in the console; put anything sensitive in app.secrets.

setasync

project_settings.set(name, value) -> None

Set name (letters, digits and _, starting with a letter; not a secret's name) to value (at most 4 KB). It reaches the app at the next deployment.

Parameters
namestrrequired
valuestrrequired
Usage
app = noclick.Project.from_name("booking")
app.settings.set("PUBLIC_BASE_URL", "https://book.acme.com")
app.deploy()

listasync

project_settings.list() -> dict[str, str]

The project's settings and their values.

Returns
dict[str, str]

Each setting's name mapped to its value.

Usage
app = noclick.Project.from_name("booking")
for name, value in app.settings.list().items():
    print(name, value)

deleteasync

project_settings.delete(name) -> None

Delete the setting name; the app stops seeing it at the next deployment. A name that isn't set raises noclick.NotFound.

Parameters
namestrrequired
Usage
app = noclick.Project.from_name("booking")
app.settings.delete("PUBLIC_BASE_URL")

versionsasync

project_settings.versions(name) -> list[Version]

What name held before each change or deletion of the last 30 days, newest first, with each value.

Parameters
namestrrequired
Returns
list[Version]

Each earlier version.

Usage
app = noclick.Project.from_name("booking")
print([v.value for v in app.settings.versions("PUBLIC_BASE_URL")])

restoreasync

project_settings.restore(name, version) -> str

Set name back to version's value (deleted since, it's back). It reaches the app at the next deployment.

Parameters
namestrrequired
versionAnyrequired
Returns
str

The value it holds again.

Usage
app = noclick.Project.from_name("booking")
app.settings.restore("PUBLIC_BASE_URL", app.settings.versions("PUBLIC_BASE_URL")[0])

ProjectAI

class ProjectAI

app.ai: noclick.AI acting as the app, from the app's own code (Project.current()). Every call is billed to your account and attributed to the app and its user: it counts toward the app's budget_usd_per_month and its user's cap, and raises noclick.SpendCapExceeded once either is used up (noclick.PaymentRequired once your account's balance is). Only the app acts as the app: on a project reached with your own key, each call raises noclick.InvalidRequest (use noclick.AI there, inside as_user for the app's user).

Attributes
base_urlstr

The OpenAI-compatible endpoint, for an OpenAI client in the app's code; pair it with headers(), which carry the deployment's credential.

chatasync

app.ai.chat(
    messages, *, model=None, max_tokens=None, temperature=None, response_format=None,
    tools=None, conversation=None
) -> ChatReply

noclick.AI.chat as the app, with the same arguments. model="agent:<name>" reaches only an agent bound to the app, in the app's own conversations.

Parameters
messagesstr | Sequence[Any]required
modelstr | Nonedefault None
max_tokensint | Nonedefault None
temperaturefloat | Nonedefault None
response_formatdict[str, Any] | Nonedefault None
toolsSequence[dict[str, Any]] | Nonedefault None
conversationstr | Nonedefault None
Returns
ChatReply

The answer: its text, the message to append, and what it cost.

Usage

Summarize a review in a request handler:

app = noclick.Project.current()
reply = app.ai.chat("Summarize in one line: the class was great but the room was cold.")
print(reply.text)

Serve the page without the summary once the app's budget is used up:

app = noclick.Project.current()
try:
    summary = app.ai.chat("Summarize in one line: the class was great but the room was cold.").text
except (noclick.SpendCapExceeded, noclick.PaymentRequired):
    summary = None

embedasync

app.ai.embed(texts, *, model=None, dimensions=None) -> Embeddings

noclick.AI.embed as the app.

Parameters
textsstr | Sequence[str]required
modelstr | Nonedefault None
dimensionsint | Nonedefault None
Returns
Embeddings

One vector per text, in order, with their length and what the call cost.

Usage
app = noclick.Project.current()
[vector] = app.ai.embed("Yoga for beginners on Saturday mornings").vectors
classes = app.database("DB").query(
    "select id, title from classes order by embedding <=> cast(? as halfvec) limit 5", [vector])

imageasync

app.ai.image(prompt, *, model=None, size=None) -> GeneratedImage

noclick.AI.image as the app.

Parameters
promptstrrequired
modelstr | Nonedefault None
sizestr | Nonedefault None
Returns
GeneratedImage

The image: a signed url that lasts 24 hours, its size and what it cost.

Usage
app = noclick.Project.current()
image = app.ai.image("A sunlit yoga studio, watercolor", size="1024x1024")
print(image.url, image.cost_usd)

transcribeasync

app.ai.transcribe(audio, *, model=None, language=None, speakers=False) -> Transcript

noclick.AI.transcribe as the app: an https URL to a recording, or its bytes.

Parameters
audiostr | bytesrequired
modelstr | Nonedefault None
languagestr | Nonedefault None
speakersbool | Sequence[str]default False
Returns
Transcript

The transcript: its text, timed segments, the language and how long the audio was.

Usage
app = noclick.Project.current()
words = app.ai.transcribe("https://booking.noclick.site/uploads/voicemail-1042.m4a", language="en")
print(words.text)

modelsasync

app.ai.models(*, kind=None) -> list[AIModel]

The models the app can call, as noclick.AI.models lists them; kind="agent" lists the agents bound to it.

Parameters
kindstr | Nonedefault None
Returns
list[AIModel]

Every matching model with its prices in US dollars, the default of each kind first.

Usage
app = noclick.Project.current()
for m in app.ai.models(kind="chat"):
    print(m.id, m.input_usd_per_token, m.output_usd_per_token)

headersasync

app.ai.headers() -> dict[str, str]

The headers an OpenAI client in the app's code sends to base_url: the deployment's credential as the bearer, so every call is the app's.

Returns
dict[str, str]

Authorization.

Usage

Code written for OpenAI's client works as the app:

from openai import OpenAI

app = noclick.Project.current()
gpt = OpenAI(base_url=app.ai.base_url, api_key="-", default_headers=app.ai.headers())
answer = gpt.chat.completions.create(
    model="openrouter/anthropic/claude-sonnet-4.6", messages=[{"role": "user", "content": "Hi"}])

Binding

class Binding

A resource the running app reads by name: a database, bucket, dict, queue, secret, connection or agent (kind), with its resource_id and resource_name.

Attributes
namestr
kindstr
resource_idstr
resource_namestr | None
created_atAny
rawdict[str, Any]

The response JSON exactly as the API sent it.

LogLine

class LogLine

One line an app printed, or its build did: when (at), its level (info, warn or error), the message, and the deployment it came from.

Attributes
atAny
levelstr
messagestr
deployment_idstr | None
rawdict[str, Any]

The response JSON exactly as the API sent it.

ProjectUsage

class ProjectUsage

What an app spent this month so far, against its budget: requests served, cpu_ms of compute, everything it cost in usd, and the period_start it counts from.

Attributes
requestsint
cpu_msint
usdfloat

Everything the app spent: hosting, builds, its calls to what it's bound to, its email, its AI and its agent turns.

period_startAny
budget_usdfloat | None

Its budget_usd_per_month (None without one).

limited_bystr | None

What refuses its paid calls now: project (its budget is used up), user (its user's cap is) or account (your account's balance is); None while they go through.

by_typedict[str, float]

usd split by the spend types of Usage.series(group_by="type"): apps, ai, model_tokens, ...

rawdict[str, Any]

The response JSON exactly as the API sent it.

DeploymentCheck

class DeploymentCheck

What Deployment.check() saw where the deployment serves (url): passed when every path did, each path's PathCheck, and what the browser cost (cost_usd, its provider's price by the second).

Attributes
deployment_idstr
urlstr
passedbool
pathslist[PathCheck]
checked_atAny
cost_usdfloat
rawdict[str, Any]

The response JSON exactly as the API sent it.

PathCheck

class PathCheck

One path a check loaded in a browser: where it ended (url), its HTTP status, how long it took to load (load_ms, None when it didn't load within 30 seconds), its page title, the errors its console showed and a screenshot. ok is False when it answered 400 or more, didn't load, or a script threw and nothing caught it; problem says which. Other console errors are reported without failing it.

Attributes
pathstr
urlstr
okbool
problemstr | None
statusint | None
load_msint | None
titlestr | None
console_errorslist[dict[str, str]]

Each {"kind", "message"}: exception (uncaught), console (console.error) or browser (a file that failed to load, ...).

screenshotdict[str, Any] | None

The screenshot, an account file the chat shows: {"id", "name", "url", "mime_type", ...} (None when the page couldn't be opened at all).

rawdict[str, Any]

The response JSON exactly as the API sent it.

RequestOutcomes

class RequestOutcomes

How the requests a deployment served between since and until went: how many, by HTTP status (by_status: {"200": 120, "500": 2, "none": 1}, none for one never answered), the errors (an answer of 500 or more, or none) and their error_rate, and the slowest_routes by their 95th-percentile time. sampled: there were more than the 2,000 newest it read, or the record of a very busy app is a sample, each request in it counted for the ones it stands for.

Attributes
requestsint
errorsint
error_ratefloat | None
by_statusdict[str, int]
slowest_routeslist[RouteTiming]
sampledbool
sinceAny
untilAny
rawdict[str, Any]

The response JSON exactly as the API sent it.

RouteTiming

class RouteTiming

One route's requests: how many, how many failed (errors), and their times in milliseconds.

Attributes
methodstr
pathstr
requestsint
errorsint
p50_msfloat
p95_msfloat
max_msfloat

ProjectAnalytics

class ProjectAnalytics

How an app's traffic went over a period (24h, 7d or 30d, from since up to until): the requests its sites answered and the errors among them (an answer of 500 or more, or none) with their error_rate, by_status ({"200": 1180, "404": 21, "none": 1}), how long answers took to begin (p50_ms, p95_ms), each interval's (hour or day, starting in tz) in series, the busiest top_paths, and the same length of time just before since (previous_requests, previous_errors) to compare with. Counted from NoClick's record of every request, its files and API calls included: requests, not visitors. sampled: a very busy app's record is a sample, each request in it counted for the ones it stands for.

Attributes
periodstr
environmentstr
intervalstr
tzstr
sinceAny
untilAny
requestsint
errorsint
error_ratefloat | None
by_statusdict[str, int]
p50_msfloat | None
p95_msfloat | None
serieslist[TrafficInterval]
top_pathslist[PathTraffic]
previous_requestsint
previous_errorsint
sampledbool
rawdict[str, Any]

The response JSON exactly as the API sent it.

TrafficInterval

class TrafficInterval

One hour or day of an app's traffic: the requests from at on, and how many of them failed (errors: an answer of 500 or more, or none).

Attributes
atAny
requestsint
errorsint

PathTraffic

class PathTraffic

One of an app's busiest paths: its requests, how many failed, and their 95th-percentile time in milliseconds.

Attributes
pathstr
requestsint
errorsint
p95_msfloat | None

AnalyticsSummary

class AnalyticsSummary

Every app's requests over a period (from since up to until, days starting in tz), read at once: apps newest first, an app nobody reached at 0. of(app) is one app's. Requests, not visitors. sampled: a very busy app's record is a sample, each request in it counted for the ones it stands for.

Attributes
periodstr
tzstr
sinceAny
untilAny
appslist[AppRequests]
sampledbool
rawdict[str, Any]

The response JSON exactly as the API sent it.

of

analytics_summary.of(app) -> AppRequests

One app's line in the summary.

Parameters
appAnyrequired
Returns
AppRequests

The AppRequests of app (a Project or its id); one the summary didn't cover reads as 0.

Usage
week = noclick.Project.analytics_summary()
app = noclick.Project.from_name("booking")
print(week.of(app).requests, week.of(app).errors)

AppRequests

class AppRequests

One app's requests to its production site over a summary's period, and how many failed.

Attributes
project_idstr
requestsint
errorsint

ProjectEmail

class ProjectEmail

app.email: transactional email from the app's own address (receipts, booking confirmations), rate-capped and charged per recipient; a high bounce or complaint rate pauses sending. One-time sign-in codes go through send_code, on a lane of their own. Newsletters and announcements go through broadcast, on a stream of their own, so they never hold up a receipt.

Usage
app = noclick.Project.from_name("booking")
email = app.email   # this app's noclick.ProjectEmail
email.enable(domain="acmebooking.com", from_address="bookings@acmebooking.com")
email.send(to="ana@example.com", subject="Your booking", text="See you Friday at 3pm.")

enableasync

app.email.enable(
    *, domain=None, from_address=None, from_name=None, postal_address=None
) -> EmailSetup

Turn email on for the app, or change what you pass (the rest keeps its value). Without a domain the app sends from its default address (noreply@booking.noclick.site). With one (a Domain or its name), mail goes from from_address on it, noreply@<domain> unless you pass one, and broadcasts go from the same name on its news. subdomain, so their reputation stays apart from receipts'. The first call answers pending while NoClick sets up the app's sending (what it sends meanwhile goes out once that's done). A domain bought through NoClick gets its DNS records written for it and turns active within minutes; any other is connected for you, and records lists what to add at its DNS host (within seconds of this call).

domain may also be a subdomain of a domain your account holds (mail.ana.acmebooking.com under the account's acmebooking.com), for any project, a user's included; only a key acting for the whole account may choose one. Its records are written for you under a bought domain, and listed in records under a connected one. postal_address is the address every broadcast's footer carries, as the law asks of bulk mail; broadcast needs one.

Parameters
domainAnydefault None
from_addressstr | Nonedefault None
from_namestr | Nonedefault None
postal_addressstr | Nonedefault None
Returns
EmailSetup

The setup: active when the app can send now, pending while NoClick sets up its sending (what it sends meanwhile goes out once that's done), or pending_dns while the domain's records aren't seen yet.

Usage

Send from your own domain:

app = noclick.Project.from_name("booking")
setup = app.email.enable(domain="acmebooking.com", from_address="bookings@acmebooking.com", from_name="Acme Booking")
print(setup.status, setup.from_address)

A domain hosted elsewhere waits for its DNS records:

app = noclick.Project.from_name("booking")
setup = app.email.enable(domain="acme.test")
for record in setup.records:
    print(record.type, record.name, record.value)

statusasync

app.email.status() -> EmailSetup

Read how the app sends email, checking the domain's DNS records again.

Returns
EmailSetup

The current setup; status is off if email was never enabled.

Usage
app = noclick.Project.from_name("booking")
setup = app.email.status()
if setup.status == "pending_dns":
    print("Still waiting for", [record.name for record in setup.records])

sendasync

app.email.send(
    to, subject, *, text=None, html=None, reply_to=None, idempotency_key=None
) -> SentEmail

Send one message to to (an address, or a list of up to 10 that each receive it) with a text body, an html one, or both. Email must be active, or pending (the message waits and goes once sending is set up): noclick.ActionRequired while the domain's records are pending, noclick.RateLimited past the daily limit.

Parameters
tostr | Sequence[str]required
subjectstrrequired
textstr | Nonedefault None
htmlstr | Nonedefault None
reply_tostr | Nonedefault None
idempotency_keystr | None

Makes a retried create safe: the same key returns the first result. Generated when omitted.

(default None)
Returns
SentEmail

The message as accepted, usually queued; messages() shows whether it was delivered.

Usage

Pass an idempotency_key so a retried request can't send twice:

app = noclick.Project.from_name("booking")
sent = app.email.send(
    to="ana@example.com",
    subject="Your booking is confirmed",
    text="See you Friday at 3pm.",
    html="<p>See you <b>Friday at 3pm</b>.</p>",
    idempotency_key="booking-1042-confirmation",
)
print(sent.id, sent.status)

send_codeasync

app.email.send_code(
    to, code, *, purpose='sign_in', expires_in_s=None, idempotency_key=None
) -> SentEmail

Send a one-time code (4-12 letters and digits) to to, in a short message NoClick writes in the app's name: purpose sign_in or verify_email, and expires_in_s said in it when you pass one. Codes have a daily limit of their own, apart from daily_limit, so a new app's sign-ins never wait on its other mail; one address gets at most 5 codes in 15 minutes (noclick.RateLimited). Each is charged as one message. app.sign_in sends its codes this way.

Parameters
tostrrequired
codestrrequired
purposestrdefault 'sign_in'
expires_in_sint | Nonedefault None
idempotency_keystr | None

Makes a retried create safe: the same key returns the first result. Generated when omitted.

(default None)
Returns
SentEmail

The message, kind code; its subject names the purpose, never the code.

Usage
app = noclick.Project.current()
app.email.send_code("ana@example.com", "482913", expires_in_s=300)

messagesasync

app.email.messages(*, to=None, limit=50, environment=None) -> list[SentEmail]

The messages the app sent, newest first; to keeps those sent to one address, and environment (production or preview) those its production or its previews sent.

A preview's code reaches only the account's own people, so environment="preview" is its outbox: each message names the recipients it kept in captured, with the text and html it would have sent.

Parameters
tostr | Nonedefault None
limitint | None

Return at most this many.

(default 50)
environmentstr | Nonedefault None
Returns
list[SentEmail]

Up to limit messages (50 unless you say; None reads them all), each with its delivery status, whether it was opened and why it bounced.

Usage
app = noclick.Project.from_name("booking")
for message in app.email.messages(to="ana@example.com", limit=10):
    print(message.at, message.subject, message.status, message.opened)
for kept in app.email.messages(environment="preview"):
    print(kept.captured, kept.subject, kept.text)

broadcastasync

app.email.broadcast(
    to, subject, *, idempotency_key, text=None, html=None, reply_to=None
) -> Operation

Send one message to many people who asked for it: a newsletter, an announcement. to holds up to 50,000 addresses, and listing them says each one opted in to mail from the app. Anyone who unsubscribed from its broadcasts (until they opt in again and you resubscribe them), bounced or marked one as spam is skipped. It goes out on a bulk stream of its own, under broadcast_daily_limit, from broadcast_from_address on the domain's news. subdomain, so a campaign's complaints never pause the app's transactional mail. It needs a domain of the app's own (noclick.ActionRequired on the default sender). Every message carries its recipient's own List-Unsubscribe header, with one-click unsubscribe, and a footer with an unsubscribe link and the postal_address from enable() (noclick.ActionRequired until there is one). Unsubscribing stops broadcasts, never receipts. It goes out at the pace mail can be sent, so a large one takes minutes.

idempotency_key names the broadcast: sending again with the same key sends nothing new, so a job that crashes halfway can simply run again.

Parameters
toSequence[str]required
subjectstrrequired
idempotency_keystrrequired

Makes a retried create safe: the same key returns the first result. Generated when omitted.

textstr | Nonedefault None
htmlstr | Nonedefault None
reply_tostr | Nonedefault None
Returns
Operation

An operation whose .wait() gives the Broadcast: how many it was sent to and who was skipped, and why.

Usage
app = noclick.Project.from_name("booking")
subscribers = [row["email"] for row in app.database("DB").query("select email from subscribers")]
result = app.email.broadcast(
    subscribers, "What's new at Acme Booking",
    html="<h1>Group bookings are here</h1><p>Book for up to 12 people at once.</p>",
    idempotency_key="newsletter-2026-10",
).wait()
print(result.sent, len(result.skipped))

resubscribeasync

app.email.resubscribe(address) -> bool

Lift address's unsubscribe from the app's broadcasts, once they've asked for them again and confirmed it: a click on a link you emailed that address, as when they first subscribed. Mail after an unsubscribe is spam unless the person opted in again, so call it only on a confirmed opt-in. A bounce or a spam complaint isn't lifted.

Parameters
addressstrrequired
Returns
bool

True if the address was unsubscribed and now gets broadcasts again, False if it wasn't unsubscribed.

Usage
app = noclick.Project.from_name("newsletter")
# Ana clicked the confirmation link you emailed her after she signed up again.
if app.email.resubscribe("ana@example.com"):
    print("Ana gets the newsletter again")

EmailSetup

class EmailSetup

How an app sends email: the sender, its domain's DNS records and whether it can send. status is off (never enabled), pending (NoClick is setting up the app's sending, usually within a minute; mail sent meanwhile goes out once it's set up, and pending_reason says why when setting it up failed), pending_dns (the domain's records aren't seen yet: a domain bought through NoClick gets them written and turns active within minutes), active, or paused (too many bounces or spam complaints; paused_reason says which: bounce_rate, complaint_rate, mailbox_provider_feedback or blocklisted). Broadcasts go out apart, from the domain's news. subdomain, with a status of their own.

Attributes
statusstr
from_addressstr | None

The address mail goes out from, like bookings@acmebooking.com.

from_namestr | None

The display name recipients see, like Acme Booking.

domainstr | None

The sending domain; None while the app uses its default sender.

recordslist[DnsRecord]

What DNS must hold for mail to send and land: DKIM and a bounce subdomain (MX and SPF) for the domain and for its news. subdomain, and a recommended DMARC policy.

daily_limitint | None

How many messages the app may send a day; it grows as its mail keeps landing.

sent_todayint | None

How many it has sent since midnight UTC.

paused_reasonstr | None
pending_reasonstr | None

Why the app's sending isn't set up yet, once setting it up failed (NoClick keeps trying); else None.

postal_addressstr | None

The postal address every broadcast's footer carries (None until you give one).

broadcast_daily_limitint | None

How many recipients its broadcasts may reach a day, apart from daily_limit.

broadcast_statusstr

needs_domain (the default sender doesn't send broadcasts), pending, pending_dns, active or paused.

broadcast_from_addressstr | None

The address broadcasts go out from, on the news. subdomain: bookings@news.acmebooking.com.

broadcast_sent_todayint | None

How many broadcast recipients it has sent to since midnight UTC.

broadcast_paused_reasonstr | None
rawdict[str, Any]

The response JSON exactly as the API sent it.

can_sendbool

Whether send goes out now (status is active).

SentEmail

class SentEmail

One message an app sent. status is queued, sent (on its way to the recipient's server), delivered, bounced, complained (marked as spam: best-effort, since some mailbox providers, Gmail among them, report no single complaint) or failed; error says why a message bounced or failed. Sent to several recipients, it shows the worst any met (a bounce, a complaint), else how far the slowest got.

A preview's code reaches only the account's own people (its owner and the members of the owner's workspaces): every other copy is kept, never sent or charged, and named in captured; a message whose every copy was kept is captured, with the text and html it would have sent. messages(environment="preview") is the preview's outbox.

Attributes
idstr
tolist[str]
subjectstr | None
statusstr
openedbool | None

Whether the recipient opened it, as far as can be told: privacy proxies open some mail on the reader's behalf. None for a message with no HTML, or a sign-in code, which aren't tracked.

bouncedbool
errorstr | None
atAny

When it was sent.

kindstr

message (send), code (send_code) or broadcast.

environmentstr

Where the code that sent it runs: production or preview.

capturedlist[str]

The recipients a preview kept rather than sent to.

textstr | None

What a kept message would have said; None once a message went out.

htmlstr | None
rawdict[str, Any]

The response JSON exactly as the API sent it.

Broadcast

class Broadcast

One broadcast an app sent: how many recipients it was sent to, and those it skipped, each {to, reason} with reason unsubscribed, bounced, complained or invalid. Each message's delivery shows in app.email.messages(). From a preview, only the account's own people get it: captured counts the copies kept.

Attributes
idstr
subjectstr | None
sentint
skippedlist[dict[str, Any]]
capturedint
rawdict[str, Any]

The response JSON exactly as the API sent it.

ProjectPayments

class ProjectPayments

app.payments: take payments in the app without a payments account of your own. NoClick opens a test account for the app and puts its keys in the app's secrets; the owner claims it to take real money.

Usage
app = noclick.Project.from_name("booking")
payments = app.payments   # this app's noclick.ProjectPayments
setup = payments.enable(country="US")
print(setup.mode, setup.claim_url)

enableasync

app.payments.enable(
    *, country=None, owner_email=None, business_name=None
) -> PaymentsSetup

Open a test payments account for the app and put its keys in the app's secrets (for production and previews). country is where the owner's business is: required the first time, since the account can only ever be in that country (noclick.NotAvailable, listing the countries, for one the processor doesn't serve). owner_email is who the processor reminds before an unclaimed test account expires (your NoClick account's email unless you pass one); business_name prefills the claim (the app's name unless you pass one).

Calling it again returns the same account, with its webhook endpoint following the app's URL. After test_expires_at passes unclaimed, it opens a fresh test account with new keys and no data, and the app must deploy again; once the owner has reinstalled NoClick's payments app on a disconnected account, it reconnects it.

Parameters
countrystr | Nonedefault None
owner_emailstr | Nonedefault None
business_namestr | Nonedefault None
Returns
PaymentsSetup

The setup in test mode, with the claim_url to send the owner.

Usage
app = noclick.Project.from_name("booking")
setup = app.payments.enable(country="US")
print(setup.secrets)   # read these in the app's code
print("Claim it before", setup.test_expires_at, setup.claim_url)

statusasync

app.payments.status() -> PaymentsSetup

Read where the app's payments stand.

Returns
PaymentsSetup

The current setup; mode moves from test to claimed to live. processor is there even while off, so an app's payment code can be written before enable().

Usage
app = noclick.Project.from_name("booking")
if app.payments.status().mode == "live":
    app.deploy()   # production picks up the live keys

processor = noclick.Project.from_name("booking").payments.status().processor   # before enable()

PaymentsSetup

class PaymentsSetup

Where an app's payments go. mode is off (never enabled), test (a test account that takes test cards, waiting to be claimed), claimed (the owner claimed it and is finishing the steps claim_step names), live (live keys are in the app's production secrets and the account can take payments) or disconnected (the owner removed NoClick's payments app or its keys). Money goes straight to the owner's account; NoClick takes no cut.

Attributes
modestr
claim_stepstr | None

While claiming: claim (send the owner claim_url), application (the owner gives the payment processor their business details), install (the owner installs NoClick's payments app on their live account) or verification (the processor is verifying the business).

next_stepstr | None

What to do now, in a sentence.

claim_urlstr | None

Where the owner claims the account: a NoClick link that always sends them to their next step, working until test_expires_at (None once live).

test_expires_atAny

When the test account and its test data are deleted unless it's claimed (60 days after enable()); after that enable() opens a fresh one, with new keys.

dashboard_urlstr | None

The owner's own dashboard at the payment processor, once claimed.

secretslist[str]

The project secrets that hold the account's keys. Production gets the live keys once live; preview deployments keep the test keys, so a preview never takes a real payment.

processorstr | None

Which payment processor apps are paid through, by the name its own client library goes by, so the app's code can pick the library that reads secrets. It's the same for every app, so status() reports it even while off, and code can be written before enable().

countrystr | None

Where the business is (ISO 3166-1 alpha-2), fixed when the account was opened.

webhook_urlstr | None

Where the processor delivers the account's events, signed with PAYMENTS_WEBHOOK_SECRET: the app's /api/payments/webhook on its production URL.

rawdict[str, Any]

The response JSON exactly as the API sent it.

ProjectSignIn

class ProjectSignIn

app.sign_in: accounts for the app's own users. Enabling it commits sign-in code into the app's repo, where you can read and change it, and keeps the users in the app's bound database.

Usage
app = noclick.Project.from_name("booking")
sign_in = app.sign_in   # this app's noclick.ProjectSignIn
setup = sign_in.enable(providers=["email", "google"]).wait()
print(setup.commit, setup.managed_providers)

enableasync

app.sign_in.enable(
    providers, *, own_credentials=None, users_table='users', database=None,
    branch=None
) -> Operation

Add sign-in with providers (email, google, apple, microsoft, github) to the app: commit the code for its framework to branch (the repo's default branch unless you pass one), create users_table and the tables it needs in its bound database and deploy. Calling it again with another list changes the providers.

google, apple and microsoft sign in through NoClick, with nothing to set up at the provider: its screen names NoClick. To show your own name there instead, list google or microsoft in own_credentials and set its client's id and secret as the app's secrets (SignInSetup.missing_secrets names them, callback_urls where to register it); apple signs in only through NoClick, and github only with your own client. Omitted, each provider keeps how it signs in now: one on your own client stays there.

The app needs a bound database (noclick.ActionRequired otherwise; database names the binding when it has several). It works on nextjs, remix and astro apps, and on a static site as it is: a worker in front of its files answers sign-in, and its pages include /auth.js (noclick.NotAvailable for a client-only vite app, or a site that already runs its own worker). email sends its codes through app.email.send_code, which is enabled with the default sender if it isn't yet.

A file the change would replace that holds edits of yours is never overwritten: the change goes on a branch of its own (SignInSetup.branch, with a preview to review), and naming a branch whose files hold edits raises noclick.Conflict naming them.

Parameters
providersSequence[str]required
own_credentialsSequence[str] | Nonedefault None
users_tablestrdefault 'users'
databasestr | Nonedefault None
branchstr | Nonedefault None
Returns
Operation

An operation whose wait() gives the SignInSetup once the code is committed and deployed.

Usage

Add it straight to production:

app = noclick.Project.from_name("booking")
setup = app.sign_in.enable(providers=["email", "google"]).wait()
print(setup.managed_providers)   # ['google']: nothing to set up

With your own Google client, to show your name on Google's screen:

app = noclick.Project.from_name("booking")
setup = app.sign_in.enable(providers=["email", "google"], own_credentials=["google"]).wait()
print(setup.callback_urls, setup.missing_secrets)

Or on a branch, to review the commit on its preview deployment first:

app = noclick.Project.from_name("booking")
op = app.sign_in.enable(providers=["email"], branch="add-sign-in")
print(op.wait().commit)

upgradeasync

app.sign_in.upgrade(*, branch=None) -> Operation

Move the app's sign-in code to the library version NoClick ships now (update_available), with the same providers: the same commit, migrate and deploy as enable, and the same care for files you edited. Security fixes come this way; NoClick tells you of one with the project.sign_in.update_available event and never pushes it to production by itself.

Parameters
branchstr | Nonedefault None
Returns
Operation

An operation whose wait() gives the SignInSetup.

Usage
app = noclick.Project.from_name("booking")
if app.sign_in.status().update_available:
    print(app.sign_in.upgrade(branch="sign-in-update").wait().commit)

statusasync

app.sign_in.status() -> SignInSetup

Read the app's sign-in: its providers, where its users are kept, and what it still needs.

Returns
SignInSetup

The current setup; status is off if sign-in was never enabled.

Usage
app = noclick.Project.from_name("booking")
setup = app.sign_in.status()
print(setup.status, setup.providers, setup.tables, setup.library, setup.version)

SignInSetup

class SignInSetup

An app's sign-in. status is off, installing (a change is being committed and deployed), active or failed (the first change failed; error says why: a later change that fails leaves sign-in active as it was, with error). The code lives in the app's repo and the users in users_table of the app's own database.

Attributes
statusstr
providerslist[str]

email (a one-time code by email), google, apple, microsoft, github.

managed_providerslist[str]

The providers that sign in through NoClick, with nothing to set up at the provider (its screen names NoClick). The rest of providers but email use your own client there.

users_tablestr | None
tableslist[str]

Every table sign-in keeps in the database: users_table first, then its sessions, the accounts people sign in with, the pending codes and its rate limits.

databasestr | None

The binding of the database the users are kept in.

librarystr

The open-source library the code is written with, and its version, so code written against the app's sign-in uses the right calls.

versionstr | None
update_availablestr | None

A newer version NoClick ships (upgrade() moves to it), else None.

commitstr | None

The commit that added or last changed sign-in.

branchstr | None

Where that commit went when it wasn't the production branch: the one you passed, or the one NoClick made because files it would change hold edits of yours (edited_files).

edited_fileslist[str]
callback_urlsdict[str, str]

Where each provider on your own client sends people back to; register it with that provider. They're on the app's own NoClick address, so adding a domain doesn't change them.

missing_secretslist[str]

Project secrets a provider on your own client still needs (GOOGLE_CLIENT_ID, ...); it works once they're set.

secretslist[str]

The secrets NoClick writes itself and never shows: AUTH_SECRET, AUTH_URL, AUTH_PROXY_SECRET, and the app's client of NoClick's sign-in (NOCLICK_SIGN_IN_CLIENT_ID, NOCLICK_SIGN_IN_CLIENT_SECRET) while any provider signs in through NoClick.

errordict[str, Any] | None
rawdict[str, Any]

The response JSON exactly as the API sent it.