inmake DOCS
Docs / SDK / Admin

Admin

Full read/write over the store: documents, collections, setup docs, files.

Admin (DropCoreAdmin)#

src/admin/drop_core_admin.ts

Resolved via getDropCoreAdmin(session?) or getDropCoreInstance().admin.

Property Type Underlying path
store Store store/{sid}
stores Stores (DropCollection<StoreEntity>) store — cross-store listing. Master-only; other roles are rejected by the access middleware.
data Data Escape hatch for free-form paths: data.collection(path) / data.document(path) for nested user collections. Also the store-wide versioned pair over FULL ref paths: data.batch(paths, {mask?, meta?}) (meta: true adds _meta[path].update_time) and data.commit(writes) — up to 100 CommitWrites (create / set / update / delete, precondition: {exists?, update_time?}) applied atomically by POST /admin/commit; a refused precondition rejects with the server's 409 {error: 'precondition_failed', index, code}.
templates Templates templates.document(locale, tid) (invoice / e-mail templates), templates.getDefault(name).
stock Stock (DropCollection<StockEntity>) stock filtered by owner == sid
promo Promo wraps the PromoCredits / PromoCoupons / PromoCodes collections under promo/{sid}

Further models live under src/admin/models/ — carts, customers, orders, products, content, files, forms, schemas, slugs, invoices — most reachable through store (below).

Store extends DropDocument<StoreEntity>#

In addition to the inherited document methods:

  • setup: StoreSetup — one typed DropDocument per configuration doc under store/{sid}/setup (see the table below).
  • content: StoreContent
    • content.collection<T>(name) — returns Record<locale, ContentCollection<T>> for every locale on the store.
    • content.document<T>(name, id) — returns a ContentDocument<T> spanning every locale.
  • customers: Customers — DropCollection<CustomerEntity> at store/{sid}/customers.
  • orders: Orders — DropCollection<OrderEntity> at store/{sid}/orders. Adds records(oid) for the $records subcollection.
  • forms: Forms — DropCollection<FormEntity> at store/{sid}/forms (the form's runtime doc: the active gate, an optional display name, and per-status summary counters). The submittable CONTRACT is not authored here: a kind:'form' schema field places the form in a document ({form: slug, fields: […]} — static fields mixed with slim {key, kind:'variable', input} entries), and each variable's constraints/strings live in the document's per-locale value as a FormVariableValue descriptor ({type, label?, required?, pattern?, pattern_message?, max_length?, …}). Assemble/validate with the re-exported resolveFormContract(fields, docValue, slug) / validateFormData(contract, data) helpers. Adds records(form) (a DropCollection<FormRecordEntity>, newest first), setStatus(form, id, status), setActive(form, active), setOrigins(form, origins) (exact-match origin allowlist for public submissions; empty = unrestricted), setColumns(form, columns) (the admin records-listing columns — FormEntity.columns, record built-ins and data.{key} paths in render order; empty = defaults), setNotify(form, config) (e-mail forwarding: FormNotifyConfig — emails, optional enabled/template/client; on record CREATE the backend renders the template — the bundled form default when unset — and mails it through the store's SMTP setup). Visitors submit via the public form.push; the formRecordChanged trigger maintains summary, fires forms hooks and sends the notify e-mail.
  • products: Products — DropCollection<ProductEntity> at store/{sid}/products. products.product(sku) returns a Product document with loadContent() (multi-locale ProductContent) and updateVariant(sku, variant, data). Product.content?.records: ProductStockRecords exposes stockUp(count).
  • files: Files — the store files tree (store/{sid}/files/...). Rich enough to get its own chapter — see Files below.
  • invoices: Invoices — DropCollection<InvoiceEntity> at store/{sid}/invoices.
  • schemas: Schemas — DropCollection<SchemaEntity> at store/{sid}/schemas (field definitions per collection; versioned web templates live in the templates subcollection of each schema doc).
  • elements: Elements — DropCollection<ElementEntity> at store/{sid}/elements, the store's prebuilt element library (reusable UI building blocks: fields + Handlebars html fragment + css/js + sample). The library's display order + shared kit code live in setup.elements (below).
  • slugs: Slugs — DropCollection<SlugEntity> at store/{sid}/slug, the store-global slug registry. routeSlugs(collection, category?) returns a RouteSlugs handle on a collection's route registry (routes/{collection}/slugs) or, with category, its category registry (routes/{collection}/routes/{category}/slugs). Registry documents are SlugEntity — bare URL-key ids holding a per-locale locales map + redirect / redirects (.dev/plans/slug-registry-locale-map.md); read them through the re-exported lookupSlug / slugRowOf rather than the top-level compat mirror.
  • addLocale(locale) / removeLocale(locale) — union/remove on locales.

StoreSetup — the store/{sid}/setup/* docs#

Every property is a typed DropDocument over one configuration doc:

property doc entity what it holds
setup.organization setup/organization TaxAddressEntity Legal/tax identity for invoicing.
setup.address setup/address AddressEntity Contact/shipping address.
setup.data setup/data DataSetupEntity collections[] — the curated registry the admin Data page lists.
setup.order setup/order OrderSetup Order/invoice numbering + order workflow config.
setup.invoice setup/invoice InvoiceSetup Invoice rendering config.
setup.hooks setup/hooks HooksSetup Outbound data-hook endpoints per event (see verifyHookSeed below for the receiver side).
setup.smtp setup/smtp SmtpSetupEntity Outgoing e-mail transport.
setup.design setup/design DesignSetupEntity The committed design system (token CSS, onboarding seeds, version); its assets field lists ingested template bundles (see Files.uploadTemplate).
setup.web setup/web WebSetupEntity Web publication: publish gate, index/header/footer refs, shell.menu, SEO, headTags, footerTags, httpHeaders.
setup.elements setup/elements ElementsSetupEntity Element-library display order/categories + the shared element-kit css/js/assets.

| setup.shipping | setup/shipping | ShippingSetupEntity | Carrier configuration — validated server-side (shared keys + carriers.{tag} blocks, no credential-looking keys; verified_at / the credentials marker are platform-written). The credentials go through secure.secret.set('carrier_{tag}', json). | | setup.delivery | setup/delivery | DeliverySetupEntity | The public delivery catalog — methods validated server-side, public: true forced. | | setup.analytics | setup/analytics | StoreAnalyticsSetup | GA4 configuration — property_id / measurement_id normalised; sa_email / verified_at platform-written. The service-account JSON goes through secure.secret.set('ga4_service_account', json). |

The three validated docs refuse a platform-written field (403 setup_field_guarded) and an unknown key (400 setup_key_unknown); the shape rules are exported from drop-core-lib (shippingSetupShape, deliveryMethodsShape, analyticsSetupShape, analyticsConfigured) so a client can check before saving.

Files (admin.store.files)#

src/admin/models/files.ts — facade over /api/admin/upload, /api/admin/folder, and the CRUD verbs for the store files tree (store/{sid}/files/..., up to 5 levels deep). Methods take a dir (a relative path under files/, e.g. 'articles/abc'); read-style methods also accept an absolute doc ref as returned by list.

method notes
upload(file, {dir?, id?, name?, variant?, metadata?, keepName?, onProgress?, signal?, resumable?, onRecord?}) Binary upload. resumable: 'auto' (default) sends files above LARGE_UPLOAD_THRESHOLD_BYTES (4 MiB) — and every file on a client without multipart (Node DropHttpClient) — through uploadLarge; smaller ones take the one-shot multipart /admin/upload (25 MB server cap; a 503 upload_busy from the core's per-instance upload gate is retried three times with a 2 / 4 / 6 s back-off — upload_retry.ts, as is writeText). true/false force the path; variant uploads are always one-shot. variant patches variants[variant] on an existing file doc. The object is stored with Content-Disposition: inline; filename="<name>", so it opens in the browser but saves under its uploaded name; keepName stores it under the slugified filename (…/urbanity-esg-report-2024.pdf) instead of the minted id — a taken basename gets a short suffix, the doc id is unaffected. Returns {path, id, entity} — an image's entity carries width / height from the server's header read (P3, 2026-10-27), on the original and on every variant.
uploadLarge(source, {dir?, id?, name?, contentType?, metadata?, keepName?, onProgress?, signal?, onRecord?, origin?}) Resumable upload up to 1 GiB (LARGE_UPLOAD_MAX_BYTES): /admin/upload/start mints a Cloud Storage session, the bytes go straight to the bucket in 8 MiB chunks (never through the API), /admin/upload/finish writes the file doc. source is a Blob/File (sliced lazily) or a Node byte source (fileSource(path) from drop-core-sdk/node). Network drops are retried and resumed in place; signal pauses (the session stays resumable for 24 h). onRecord hands you the ResumableUploadRecord right after start — persist it to resume after a reload. Same destination rules and result as upload.
resumeUpload(record, source, {onProgress?, signal?}) Continue an interrupted uploadLarge with the same file (size must match): asks GCS what it holds, sends the rest, finishes.
abortUpload(record) Cancel the GCS session and drop the server session; bytes that already landed are reaped unless a file doc references them.
uploadTemplate(file, {id?, name?, dir?}) Zip upload with server-side unzip (unzip: 'design'): extracted entries land in Storage, ONE type:'archive' manifest doc is written into the files tree (default dir templates), and a DesignAssetRef is appended to setup/design.assets. Browser-only (multipart).
writeText({dir?, id?, name, content, contentType?, metadata?, keepName?}) Create/replace a TEXT file via the JSON/base64 shape of /admin/upload — works in Node too. Same id replaces in place (stable URL); text uploads get a short (5 min) cache-control.
createBundle(name, {id?, dir?}) Create an EMPTY type:'archive' bundle (zero-entry manifest + its DesignAssetRef) — the bootstrap writeArchiveEntry needs. Idempotent per id.
writeArchiveEntry(archiveLocator, entryPath, content) Overwrite/create ONE text entry of an ingested bundle; upserts the manifest row and bumps the bundle's DesignAssetRef.version. Entry extensions ride the bundle allow-list; max 4 MB.
removeArchiveEntry(archiveLocator, entryPath) Drop one manifest row (the filesChanged trigger reaps the orphaned Storage object); bumps the bundle version.
folder(dir, name?) mkdir -p along dir (root rejected — the root collection has no doc).
list(dir = '', {sort?}) Immediate children; each FileNode carries id/ref + an SDK-computed dir. Sorted folders-first/alphabetical unless {sort: false}.
get(locator) One file/folder doc, undefined when missing.
setMetadata(locator, metadata) Patch the doc's metadata map.
setVariants(locator, variants) REPLACE the whole variants map — omitting a key drops that variant (triggers delete the orphaned Storage object).
delete(locator) Delete a file or folder; folder deletion cascades via the filesChanged trigger.

Static helpers (exposed so UI layers don't duplicate the rules): Files.publicUrl(storageBase, storage_path) (files store only bucket-relative paths — compose URLs per environment), Files.validateSegment(name) / Files.joinDir(...) / Files.dirSegments(dir) (path-segment rules, mirror the server's FILE_PATH_SEGMENT_RX + FILE_PATH_MAX_DEPTH), Files.textContentType(name), Files.isImage(node), Files.sortNodes(nodes).

When deriving a variant/file location, use the node's storage_path — a FileNode.dir includes the leaf id.

The resumable transport (src/admin/models/resumable_upload.ts, browser-safe, global fetch) is exported for hosts that drive a session themselves: ResumableUpload(sessionUri, source, chunkBytes, {onProgress, signal, retries, fetch}) with run(resume?) / status() / cancel(). Failures are ResumableUploadError with a reason: expired (404/410 — the session is gone, start over), conflict (412 — another upload replaced the object meanwhile), failed (a non-retryable status), aborted (the signal fired). ResumableUploadRecord carries {sid, upload, id, path, dir, name, size, storage_path, session_uri, chunk_bytes, fingerprint, expires} — session_uri is a write credential for that one object, keep it private; fingerprint (name|size|lastModified) matches a re-picked file.

import {fileSource} from 'drop-core-sdk/node';

const files = core.admin.store.files;
const result = await files.uploadLarge(await fileSource('./media/showreel.mp4', {type: 'video/mp4'}), {
    dir: 'video',
    onProgress: (sent, total) => console.log(`${Math.round(sent / total * 100)} %`),
});
console.log(result.entity.storage_path);

Localized content (ContentCollection, ContentDocument)#

ContentCollection<T> is just a DropCollection scoped to store/{sid}/data/{locale}/{collection}. It adds documentPath(id) and the revision history reads revisions(id, limit = 20) (newest first, one list read) / revision(id, rev) over the server-written …/{id}/$revisions subcollection (.dev/plans/document-editor-durability.md decision 3; writes there are refused for every token but master — restoring = writing the document). ContentDocument<T> keeps a parallel DropDocument per locale and offers:

  • of(locale) — the underlying DropDocument<T> for that locale.
  • addLocale / removeLocale.
  • getRefs() — { ref: locale } map for batch fetches.
  • load() — batch-loads every locale and returns a Record<locale, T> map.
  • set(locale, data, merge?), update(locale, data), delete(locale).

Query keys#

A number is aggregate(), a chart is series(). On any admin collection (Orders, Invoices, a ContentCollection, …) aggregate({sum, avg}) answers {count, sum, avg} for the active where in one aggregation query, and series({start, count, stepSeconds?, field?}, {sum, avg}) answers the same per time bucket (buckets: [{count, sum, avg}, …], null on an API that predates it) — 1 + min(matching docs, count) reads, so a 28-day chart never lists the 28 days' documents.

Most listings need nothing. A list, count or aggregate that combines == filters with a range or an order_by on ANOTHER field ("articles of category x, newest first") runs on a query key of the collection, and the common shapes are built in: every content collection carries status_timestamp, status_{d} for its first two datetime fields and, for its first enum field e, {e}_{d1} + status_{e}_{d1} (d1 = the first datetime field, else timestamp) — recipes with an enum category get status_timestamp, category_timestamp, status_category_timestamp; articles with a publish_date get status_timestamp, status_publish_date. The products mirror carries status_price, status_name, status_timestamp, and its category / tag pages (QueryObject.arrayContains on categories / tags + orderBy price, name or timestamp, plain or with status: 'public') run on platform composite indexes — one array filter per query.

// built in — nothing to declare
const recipes = getDropCoreAdmin().data.collection('store/instore/data/cs_CZ/recipes');
recipes.where({ status: QueryObject.equalTo('public'), category: QueryObject.equalTo('soup') });
recipes.orderBy({ timestamp: 'desc' });
await recipes.load(); // → key status_category_timestamp

A shape beyond the built-ins (a number sort such as prep_time, a second enum) needs an explicit key: a named, ordered field list on the collection's setup/data row (keys: {status_prep_time: ['status', 'prep_time']}; ≤ 8 explicit keys on their own budget, 1–4 fields each, dotted paths allowed, names ^[a-z][a-z0-9_]{0,31}$, never id or _k; null disables a built-in of that name, the same name overrides it). The platform stamps _k.{name} on every document and rewrites a matching query — equality on the leading fields, range and/or order_by on the last — onto it; it ships no composite index for a store's collections. An equality-only query and a range + sort on one field need no key at all. Without a matching key the query still runs while its equality part matches ≤ 500 documents (sorted in memory); above that the server answers 400 {error: 'query_key_required', collection, suggest: ['status', 'prep_time']} — the SDK rejects with an Error whose .status is 400 and whose .body is that object. The fix is a key with exactly suggest (a data collection: the console's collection settings → Sortable filters, or registerCollection keys over MCP; a module's records: the manifest's collection.keys; products: the products collection row (setup/data.collections[key = "products"].keys)) — never a smaller listing. A key backfills asynchronously (building → ready; a built-in on the first listing after it appears); until ready the query keeps its native/fallback path. Never write _k (it is stripped and recomputed) — it never appears in public reads or hook payloads. Composite indexes remain only for the fixed core collections (orders, stock, log, the products array filters); a missing one there is 400 {error: 'index_required', collection}. To see which plan served a query, a master caller sends x-drop-read-profile: 1 and reads the x-drop-query-plan response header (key:{name} | native | fallback).

Stock and the promo collections#

All are plain DropCollections preconfigured to the right path. PromoCredits.push(uid, value, { id?, note? }) writes a StockRecordType.stock record into promo/{sid}/credits/{uid}/records. PromoCredits.records(oid) lists records ordered by timestamp desc.

PromoCoupons is direct CRUD on coupon batches (promo/{sid}/coupons, newest-first); PromoCodes is the flat coupon-code index (promo/{sid}/codes), with forBatch(batch, { limit?, offset? }) loading one batch's codes via a single-field batch == query (status filtering / ordering is client-side — no composite index). Batch management rides this generic CRUD; minting / verifying / redeeming codes is the promo facade's coupons module (below).