Pembangun
// sdk reference

SDK Reference.

Digylearn Page SDK — API Reference

The Digylearn Page SDK is a small vanilla-JavaScript client auto-injected into every published page. It lets your site read collections, submit records, and handle visitor login — all same-origin, no build step.

The SDK is exposed as window.PageSDK. All methods return a Promise that resolves with parsed JSON on a 2xx response and rejects on any non-2xx (the rejection value is the parsed JSON body, or an { error } object).

Availability across render modes. window.PageSDK is injected on both sanitized-mode and code-mode pages. However, in sanitized mode (digy_update_page) all <script> tags are stripped on render, so you cannot call the SDK from JavaScript. To use the SDK from client-side code, author the page with digy_write_page_code (code mode) — your JavaScript runs as-is behind a strict Content-Security-Policy whose connect-src 'self' permits same-origin SDK calls. See the build guide for render-mode details.

Configuration

On load the SDK reads window.PAGE_SDK_CONFIG (injected by the server) into PageSDK.config:

window.PAGE_SDK_CONFIG = { siteSlug: "my-portfolio" };
PageSDK.config = { siteSlug: "my-portfolio" };

All request URLs are built from the site slug: base = /api/public/sites/${siteSlug}. Requests are relative (same-origin) and always send X-Requested-With: XMLHttpRequest and Accept: application/json.

PageSDK.records.list(collectionSlug, [options])

Reads records from a collection. Anonymous visitors can read public collections; authenticated collections require a logged-in visitor; private collections are never readable via the public API.

const { collection, count, records } = await PageSDK.records.list("testimonials", {
  filter: { approved: true }, // optional: exact-match field filters
  limit: 20,                  // optional, max 200, default 50
});

// records: [{ id, data, created_at }, ...]
  • filter — an object of { field: value } exact-match conditions.
  • limit — capped at 200, defaults to 50. Records are newest-first.
  • Resolves: { collection, count, records: [{ id, data, created_at }] }.

PageSDK.records.create(collectionSlug, data)

Submits a new record. Requires the collection write rule to permit the caller (public = anyone, authenticated = logged-in visitor, private = never via public API). Respects the site's plan total-entries limit.

const { success, record } = await PageSDK.records.create("contact-submissions", {
  name: "Ada",
  email: "ada@example.com",
  message: "Hello!",
});

// record: { id, data, created_at }
  • data — an object (or array) of fields to store.
  • Resolves (HTTP 201): { success: true, record: { id, data, created_at } }.

PageSDK.records.update(collectionSlug, recordId, data, [options])

Updates an existing record by id. Requires the collection write rule to permit the caller (same tiers as create). Supports two modes:

// Merge (default): shallow-merge the given fields over the existing data.
const { success, record } = await PageSDK.records.update("contact-submissions", 42, {
  status: "read",
  read_at: "2026-08-20",
});

// Replace: overwrite the whole data object with the given one.
await PageSDK.records.update("contact-submissions", 42,
  { status: "archived", note: "moved" }, { mode: "replace" });

// record: { id, data, created_at, updated_at }
  • data — object of fields to store. In merge mode, keys present here overwrite existing keys and any keys not mentioned are preserved; in replace mode the whole data object is overwritten.
  • options.mode — "merge" (default) or "replace".
  • Resolves (HTTP 200): { success: true, record: { id, data, created_at, updated_at } }. updated_at advances on every change.

PageSDK.records.delete(collectionSlug, recordId)

Deletes a record by id (hard delete — there is no trash/undo via the public API). Requires the collection write rule to permit the caller.

const { success, deleted } = await PageSDK.records.delete("contact-submissions", 42);

// deleted: { record_id, collection }
  • Resolves (HTTP 200): { success: true, deleted: { record_id, collection } }.
  • Deleting an already-deleted id returns 404 not_found (not idempotent via the public API).

PageSDK.auth.me()

Returns the current visitor's login state (resolved from the httpOnly session cookie — the token itself is never readable by JS).

const { authenticated, end_user } = await PageSDK.auth.me();

if (authenticated) {
  // end_user: { id, email, username, role, active }
}
  • Resolves: { authenticated: false } when anonymous, or { authenticated: true, end_user: { id, email, username, role } } when logged in.
  • The SDK refreshes its cached login flag by calling this on load.

PageSDK.auth.login(identifier, password)

Logs a visitor in. Only works when the site has visitor auth enabled (configure_site_auth). identifier is matched against the enabled auth methods (email and/or username). Passwords are verified server-side with Hash::check.

const { success, end_user } = await PageSDK.auth.login("ada@example.com", "secret");

// end_user: { id, email, username, role, active }
  • Resolves: { success: true, end_user: { id, email, username, role, active } }.
  • Sets the digy_visitor httpOnly, SameSite=Lax session cookie (30-day expiry). The browser never sees the token. The cookie uses path /, so the visitor session persists across every page on the same site subdomain without re-logging in.
  • Rejects (403) when auth is disabled, or when the account is deactivated (code: "account_disabled"); rejects (401/422) on bad credentials.

PageSDK.auth.register(payload)

Registers a new visitor account from the published site. Only works when the site has self-registration enabled (configure_site_auth → registration.enabled). Which identifiers may be registered is set by the site's registration.methods (email and/or username).

// Approval mode (default): the account is created but stays inactive until an
// admin activates it. The response is pending and no session is set.
const { success, pending, end_user } = await PageSDK.auth.register({
  email: "ada@example.com",
  password: "secret123",   // min 8 characters
});
// end_user: { id, email, username, role, active }

// Open mode: the account is active immediately and the visitor is logged in.
const { success, authenticated, end_user } = await PageSDK.auth.register({
  email: "ada@example.com",
  password: "secret123",
});
  • payload — { email?, username?, password }. Send only the identifiers enabled for registration; at least one is required.
  • Resolves (approval): { success: true, pending: true, end_user: { id, email, username, role, active } } (HTTP 201, active: false, no cookie).
  • Resolves (open): { success: true, authenticated: true, end_user: { id, email, username, role, active } } (HTTP 200, active: true, session cookie set — same as login).
  • Rejects (403) when registration is disabled (code: "registration_disabled"), the plan's end-users feature is off, or the site entries limit is reached (code: "permission_denied").
  • Rejects (422) when an identifier is already taken (code: "identifier_taken"), the password is too short, or an identifier is missing.
  • Self-registration can never grant the admin role; the default role is set by the site (registration.default_role, default member).

PageSDK.auth.logout()

Ends the current visitor session (deletes the server session row and clears the cookie).

const { success } = await PageSDK.auth.logout(); // { success: true }

PageSDK.auth.isLoggedIn()

Synchronous cached flag, kept in sync by me(). Useful for quick UI checks without awaiting a request.

if (PageSDK.auth.isLoggedIn()) { /* show logged-in UI */ }

PageSDK.admin.listEndUsers()

Lists the site's end users. Requires the current visitor to be an active site admin (an end_user with role=admin for this site — never a platform user). Use this to build a custom admin dashboard on the published site to approve/activate members.

const { end_users } = await PageSDK.admin.listEndUsers();
// end_users: [{ id, email, username, role, active, created_at }, ...]
  • Resolves: { end_users: [{ id, email, username, role, active, created_at }] }.
  • Rejects (403) when the visitor is not an admin for this site.

PageSDK.admin.updateEndUser(id, changes)

Updates an end user's access and/or role. Requires an active site admin. Disabling a user (active: false) revokes their sessions immediately.

// Approve/activate a self-registered member:
await PageSDK.admin.updateEndUser(42, { active: true });

// Promote to admin:
await PageSDK.admin.updateEndUser(42, { role: "admin" });

// Deactivate:
await PageSDK.admin.updateEndUser(42, { active: false });
  • changes — { active?, role? } where role is "visitor" | "member" | "admin".
  • Resolves: { success: true, end_user: { id, email, username, role, active } }.
  • Guards: an admin cannot deactivate or demote themselves, and cannot demote the last remaining active admin (→ 422 validation_failed).
  • Rejects (403) when the visitor is not an admin for this site.

Page access levels

Every published page has an access level (write_page_code / update_page access arg, or the dashboard page editor):

Level Who can view the published page
public Anyone (default)
authenticated Any active logged-in end_user of the same site (visitor, member, or admin)
admin Active end_user with role=admin for the site

Access is enforced server-side on every page render (subdomain and custom domain). When access is denied:

  • Anonymous visitors see a themed "Log in required" page (403).
  • Logged-in visitors who lack permission (e.g. a member on an admin page) see a themed "Access denied" page (403).

The owner builds the actual sign-in / registration UX on a code-mode page (using PageSDK.auth.login / register). The themed denied-pages only inform the visitor.

Access-controlled pages are never served from the full-page file cache: they are composed fresh per request and sent with Cache-Control: private, no-store and an ETag that varies by the visitor's access bucket (anon / auth / admin), so members-only HTML can never leak to anonymous visitors through a shared cache.

Access-rule behavior

Each collection has access_rules with read and write keys, each one of public | authenticated | private:

Value read write
public Anonymous visitors OK Anyone can submit
authenticated Only logged-in visitors of the same site Only logged-in visitors of the same site
private Never via public API (owner-only, MCP/dashboard) Never via public API (owner-only, MCP/dashboard)

Default (when a collection has no rules): { read: "public", write: "private" }. Rules are enforced server-side on every request, so they cannot be bypassed from the browser.

Error format

Non-2xx responses reject the returned Promise. The rejection value is the parsed JSON body when available, otherwise { error: "<status text>" }. The body always carries an error string (read this for a human message) plus stable machine-readable siblings:

  • success — always false on errors.
  • error — human-readable string (backward compatible; keep reading this).
  • code — machine-readable error code (see table below).
  • fields — present only on validation errors (code: "validation_failed"); an object of { field: [messages] }.
Status code Meaning
403 permission_denied Access rule denied, feature disabled, or site plan limit reached
403 registration_disabled Self-registration (or visitor login) is not enabled for this site
403 account_disabled Login attempted with an account that an admin has deactivated
404 not_found Site not published, or collection/record slug or id not found
419 — Mutating request (POST/PATCH/DELETE) missing the X-Requested-With header
422 validation_failed Request input invalid (fields lists the offending fields)
422 identifier_taken Registration: the email or username already exists on this site
422 too_large Record data payload exceeds the 256 KB limit
429 rate_limited Rate limit exceeded (login / register / record create/update/delete are throttled)
401 — Invalid login credentials
try {
  await PageSDK.records.create("private-col", { a: 1 });
} catch (err) {
  console.error(err.error); // e.g. "This collection does not allow public writes."
  if (err.code === 'validation_failed' && err.fields) {
    console.warn(err.fields); // { data: ["The data field is required."] }
  }
}

Reserved page slugs

The following slugs are reserved by the platform and cannot be used as a page (or site) slug — they route to platform URLs (/api/…, /dashboard, etc.) and would collide with your content:

api  admin  dashboard  assets  storage  login  register  logout  mcp

If you want a page that conveys one of these words, pick an alternative such as sign-in (instead of login), media (instead of assets), control-panel (instead of admin), or data-api. Page creation rejects reserved slugs with a validation error.

Combining the SDK with custom JavaScript

On a code-mode page (digy_write_page_code) your own JavaScript runs alongside window.PageSDK. A common pattern is to keep UI state in memory and persist it via the SDK. For example, a simple order form that reads a menu collection and submits an order:

// 1. Load menu items from a public collection
const { records } = await PageSDK.records.list('menu', { limit: 50 });

// 2. Build cart state in memory
const cart = {};
function add(id) { cart[id] = (cart[id] || 0) + 1; render(); }

// 3. Submit the finished order to a public-write collection
async function checkout(customer) {
  await PageSDK.records.create('orders', {
    customer,
    items: cart,
    total: Object.entries(cart).reduce((s, [id, q]) => s + priceOf(id) * q, 0),
  });
  // Then hand off to WhatsApp via navigation (CSP-allowed):
  window.location.href = 'https://wa.me/60123456789?text=' + encodeURIComponent(JSON.stringify(cart));
}

Both the SDK call (connect-src 'self') and the WhatsApp navigation succeed under the code-mode CSP. See the build guide for the full list of CSP constraints.

Security notes

  • The session token lives only in an httpOnly cookie — JavaScript (and the SDK) cannot read it.
  • Mutating endpoints require the X-Requested-With header as a CSRF defense (the SDK sends it automatically; plain cross-site forms/images cannot).
  • tenant_id / site_id are never trusted from the client — they are always derived from the request host and the slug.

Disampaikan daripada sumber yang sama dengan tool MCP <code class="text-lime">digy_get_sdk reference</code>.

Kembali ke gambaran keseluruhan →