SDK

Official libraries for calling JustDeploy from the code you deploy. They cover your database, your files, and sending email, and a deployed app needs no keys at all: JustDeploy puts an identity in the image and the library uses it.

They are for server code. If you are only deploying an app and not calling JustDeploy from inside it, you do not need any of this.

Install

Node.js 22, 24 or 26
npm install @justdeploy/sdk@0.2.2
Python 3.12, 3.13 or 3.14
python -m pip install justdeploy-sdk==0.2.2

The Python package installs as justdeploy-sdk and imports as justdeploy. Both are published from our own repository and carry the same version.

How it signs in

Construct the client with no arguments. There is no API URL, no key, and no configuration file to pass.

JavaScript and TypeScript
import { JustDeploy } from '@justdeploy/sdk'

const justdeploy = new JustDeploy()
const result = await justdeploy.databases.query('your-database-id', 'SELECT * FROM orders')
Python
from justdeploy import JustDeploy

with JustDeploy() as justdeploy:
    result = justdeploy.databases.query("your-database-id", "SELECT * FROM orders")

Python also ships AsyncJustDeploy with the same methods, awaited.

It looks in one fixed order:

  • Both JUSTDEPLOY_ACCESS_KEY and JUSTDEPLOY_SECRET_KEY in the environment: it trades them for a short-lived session. This is how you run the code on your own machine.
  • Otherwise the deployment identity JustDeploy adds while building. This is the path for a deployed app, and it needs nothing from you.
  • Neither one present: it stops with an authentication error rather than guessing.

One variable set without the other is an error, and a rejected key never quietly falls back to the deployment identity. An app that already has keys registered keeps using them, and nothing about your build changes if you leave them in place.

A client on your own machine always talks to production, so use ids from the same organization. Keep the two values out of your source and out of the zip you deploy: the library does not read a .env file itself. Your local runner may, so keep that file out of version control.

What you can call

Three areas, named the same way in both languages. Python method names are in snake case, so listTables is list_tables.

Database
justdeploy.databases.list()
justdeploy.databases.query(databaseId, 'SELECT * FROM orders')
justdeploy.databases.listTables(databaseId)
justdeploy.databases.createTable(databaseId, { ... })
justdeploy.databases.updateTable(databaseId, 'orders', { ... })
justdeploy.databases.deleteTable(databaseId, 'orders')
Storage
justdeploy.storages.list()
justdeploy.storages.upload(storageId, { ... })
justdeploy.storages.createUploadUrl(storageId, { name: 'image.jpg', mime: 'image/jpeg' })
justdeploy.storages.listFiles(storageId, { limit: 50 })
justdeploy.storages.getFile(storageId, fileId)
justdeploy.storages.download(storageId, fileId)
justdeploy.storages.deleteFile(storageId, fileId)
Email
justdeploy.mail.send({ ... })
justdeploy.mail.list({ limit: 50 })
justdeploy.mail.get(mailId)

Two things behave differently from what the shapes suggest. query takes data statements only, so use the table methods to change a schema. And an upload gives you a file id, not a lasting URL: store the id, then call getFile when you need a fresh link.

Pass an idempotencyKey when sending email if a retry is possible, and remember that a successful send means accepted rather than delivered. Read status back later if delivery matters.

Keep SQL and input values separate. Use one unquoted ? per value, never your own escaping or string interpolation. Each call is one statement; separate calls are not a transaction.

JavaScript and TypeScript
const result = await justdeploy.databases.query(databaseId,
  'SELECT * FROM orders WHERE customer = ?', { params: [customer] })
Python
result = justdeploy.databases.query(database_id,
    "SELECT * FROM orders WHERE customer = ?", params=[customer])

Parameters accept strings, finite numbers, booleans and null. Send large integers and exact decimals as strings. Keep table and column names fixed. LIMIT ? is not supported: validate a bounded integer before placing a LIMIT number in SQL.

Browser files go directly to Storage. After checking your user’s upload permission, the app server calls createUploadUrl (Python: create_upload_url) and returns only the upload information with Cache-Control: no-store. The SDK stays on the server.

App server
const upload = await justdeploy.storages.createUploadUrl(storageId, {
  name: 'image.jpg', mime: 'image/jpeg',
})
// Return { fileId, url, method, headers, expiresAt } to the authorized browser.
Browser
const response = await fetch(upload.url, {
  method: upload.method, headers: upload.headers, body: selectedFile,
  credentials: 'omit', redirect: 'error',
})
if (!response.ok) throw new Error('Upload failed (' + response.status + ').')
// Save upload.fileId through your app API. No active-state wait is needed.

Do not send the browser a Credential or session token, and do not add Authorization or Content-Length to the upload. The URL is temporary permission, not a one-use link or an enforced size limit. Never log or save it. Every preparation creates a new file, so do not blindly retry a lost response.

Use upload when your server already has the bytes or a stream. A successful PUT means the bytes arrived even if metadata is still pending. For browser downloads, use getFile to obtain a URL and redirect instead of buffering the file on your server.

A successful delete accepts the deletion; GET and list metadata may update later. An immediate stale read is not a delete failure. Failed uploads try to remove their pending records without replacing the safe SDK transfer error or cancellation with a cleanup error.

When a call fails

Handle SDK failures with JustDeployError. API errors can include status, retryAfter, requestId, and details. For diagnostics, log only status and requestId, never the whole error, request, SQL, file contents, or credentials. A canceled request may already have changed server data; never blindly retry a write.

Error diagnostics prefer valid JSON fields and fall back to Retry-After and X-Request-Id headers, including when the response is not JSON. Retry delays are positive whole seconds. A delay does not automatically repeat a request. Only GET requests can refresh authentication after a 401 and retry once; writes are never automatically repeated.

Authentication allows 10 seconds and each ordinary API request allows 30 seconds. These are separate limits, not a total-call guarantee. Account for both when setting an app deadline; do not remove all deadlines to hide a timeout.

What it does not cover

  • Server code only. Not browsers, not edge runtimes, not Deno or Bun. A key in browser code is a key you have given away.
  • Node.js and Python only. For anything else, call the API with the header described in Authentication.
  • Database, files, and email. Builds, projects, logs, and everything else stay on the API.
  • No command line tool, and no way to send an arbitrary request to a URL of your choosing.

Presigned uploads and downloads never carry your credentials, so a file transfer cannot leak them. Asking for a file whose upload is still finishing gives you a retry-later error rather than a half-written response. See Authentication for the raw header, and Credentials for managing keys.