eekaam.docs

Apps

App review guidelines


App review guidelines

Every app listed on Eekaam is reviewed before merchants can install it, and so is every update. This page is the list reviewers work from. Each rule has an id, such as ui.kit. The same ids appear in three places: here, in the output of eekaam app check, and in the notes a reviewer writes when rejecting a submission.

How review works

  1. You submit. eekaam app submit, or Submit for review on the partner dashboard's Release screen. The submission is a snapshot of the app's configuration at that moment. Before it is accepted, the dashboard's release checklist must pass — see Building an app.
  2. A reviewer checks it. They install the app on a test store, walk through it, and go through every rule on this page, the automated ones included.
  3. The reviewer approves or rejects it.
    • Approved: the version is released. New installs get it, and existing merchants move to it — unless it asks for new scopes, which each merchant approves first.
    • Rejected: the reviewer's notes say what to change and cite the rule ids that failed, for example "ui.no-browser-dialogs: settings page calls confirm() before deleting." A rejection always has notes.

Rejecting an update does not touch the version merchants already have. They keep running the last approved release while you fix the next one.

Fix what the notes cite, run eekaam app check again, and resubmit.

eekaam app check

eekaam app check runs the automated rules against your app project, on your machine, before a reviewer ever sees it.

FlagDefaultWhat it does
--jsonoffPrint the report as JSON
--path.The app project directory. The CLI walks up from here to the nearest eekaam.app.toml

It reads the manifest — app_url, embedded_url, [auth] redirect_urls and allowed_origins, [[webhooks]] — and the project's source files, skipping node_modules, build, dist and hidden directories. Comments and the text inside strings are ignored where a rule looks for code. It does not deploy, call your app, or change anything.

Each automated rule ends in one of these states:

StatusMeaning
passThe rule holds
failAn error: the rule is broken, and submission is blocked
warnA warning: reported so you can fix it; it does not block submission
skipThe rule does not apply — for example, no webhooks are declared

Failures and warnings list their findings with the file and line where there is one, and link to the rule on this page. Rules only a reviewer can check are listed as manual in the JSON and counted in the text output.

eekaam app check exits non-zero when at least one rule fails, and zero when there are only warnings or nothing at all. Use it in CI:

The JSON report looks like this:

eekaam app check
eekaam app check --json            # machine-readable, for CI
eekaam app check --path ./my-app   # check another directory
eekaam app check || exit 1
{
  "root": "/home/you/reviews-app",
  "rules": [
    {
      "id": "ui.no-browser-dialogs",
      "title": "No alert(), confirm() or prompt()",
      "kind": "auto",
      "severity": "error",
      "status": "fail",
      "doc_url": "https://docs.eekaam.com/app-review-guidelines#uino-browser-dialogs",
      "findings": [
        { "file": "app/routes/app.settings.tsx", "line": 42, "message": "confirm() — use <e-modal> for confirmations and toast() for messages" }
      ]
    }
  ],
  "errors": 1,
  "warnings": 0
}

During submit

eekaam app submit runs the same checks first. If any rule fails, it prints the report and does not submit. Warnings are printed, and the submission goes ahead.

--skip-checks only skips the local run. Reviewers check every rule regardless, so skipping a failing check moves the rejection later; it does not avoid it. Use it when a check is wrong about your project, and say why in your submission.

eekaam app submit                 # checks, then submits
eekaam app submit --skip-checks   # submits without running them

The rules

IdKindSeverity
ui.kitAutomatederror
ui.no-browser-dialogsAutomatederror
ui.no-top-navigationAutomatederror
ui.save-barManual
ui.native-overlaysManual
ui.loading-statesManual
security.httpsAutomatederror
security.embedded-originAutomatederror
security.no-client-secretAutomatederror
security.webhook-signatureAutomatederror
security.session-tokenManual
data.uninstallAutomatedwarning
data.minimal-scopesManual
listing.completeManual

Automated rules are checked by eekaam app check, and again by the reviewer. An error blocks eekaam app submit; a warning is only reported.

Manual rules need a person. eekaam app check cannot test them; it only lists them as left to the reviewer. A reviewer rejects a submission that fails one.

UI rules

Merchants should not be able to tell where the admin ends and your app begins. See the App UI kit and App Bridge.

ui.kit

Automated · error

The app loads the Eekaam UI kit: ui.js from the Eekaam CDN, or a dependency on @eekaam/ui-react or @eekaam/ui.

  • Passes: a source file references the kit's CDN URL (themes-cdn.eekaam.com/ui/v1/ui.js), or a package.json in the project depends on @eekaam/ui-react or @eekaam/ui.
  • Fails: neither — the app draws its own buttons, fields and cards.
  • Fix: add the script tag to your page <head>, and build screens from <e-*> elements. In React, install @eekaam/ui-react and keep the script tag. See Load ui.js.

ui.no-browser-dialogs

Automated · error

No alert(), confirm() or prompt().

  • Passes: confirmations use <e-modal>, and messages use toast().
  • Fails: a call to the global alert(), confirm() or prompt() in the app's source, including window.confirm(), globalThis.confirm() and self.confirm(). Methods with the same name (modal.confirm()) and your own functions called confirm are fine. Browser dialogs inside a frame show the app's origin in their title, block the whole admin, and look nothing like it.
  • Fix: replace confirm() with an <e-modal> whose primary action does the work. Replace alert() with toast(), or with an <e-banner> when the message must stay on screen. Replace prompt() with an <e-modal> holding an <e-text-field>.

ui.no-top-navigation

Automated · error

No window.top.location and no target="_top", except in the route that starts the install.

  • Passes: the app moves the merchant around the admin with navigate() or <e-link admin-path>. The install flow may break out of the frame, because the approval screen refuses to be framed: files whose path contains auth (for example app/routes/auth.start.tsx and auth.callback.tsx) are allowed to.
  • Fails: top.location or a "_top" target in any other file. Leaving the frame throws away the admin's state and skips its unsaved-changes guard.
  • Fix: call navigate("/orders/123") for admin pages. For your own pages, navigate inside the frame. For external sites, open a new tab with <e-link external>.

ui.save-bar

Manual

Forms with unsaved changes use <e-save-bar>, so the admin blocks leaving.

  • Passes: editing a settings form shows the admin's "Unsaved changes" bar, and trying to leave the page asks first. Save and Discard work from the admin's bar.
  • Fails: edits are lost silently on navigation, or the app draws its own "unsaved changes" bar.
  • Fix: add <e-save-bar watch-form="your-form-id"> and close it once the save succeeds. See Save bar. Settings that apply the moment they change — an <e-switch> saved on toggle — do not need one.

ui.native-overlays

Manual

Confirmations use <e-modal>, transient messages use toast(). No home-made overlays.

  • Passes: destructive actions are confirmed in an <e-modal>. "Saved" and similar messages appear as admin toasts. Menus and popovers use <e-menu> and <e-popover>.
  • Fails: the app draws its own modal, toast, snackbar or dropdown with its own styles, or uses a third-party dialog library.
  • Fix: swap them for the kit's components. Keep modal bodies to plain text where you can, so the admin draws them over the whole window.

ui.loading-states

Manual

Loading shows skeletons or spinners, never a blank frame. First paint within 3 seconds.

  • Passes: something the merchant can see — <e-skeleton> in the shape of the content, or <e-spinner> — appears within 3 seconds of opening the app, and every later wait shows one too.
  • Fails: a white frame while data loads, text that jumps into place, or a first paint that takes longer than 3 seconds on the reviewer's test store.
  • Fix: render the page's shell with skeletons straight away and fill it in as data arrives. Use loading on <e-data-table>, <e-button> and <e-modal>. Move slow work off the first request.

Security rules

security.https

Automated · error (localhost is a warning)

app_url, embedded_url and every redirect URL are https://.

  • Passes: every one of those URLs in eekaam.app.toml is an absolute https:// URL. Skipped when the manifest has none.
  • Fails: any of them uses http:// on a real host, or is not an absolute URL. A URL on localhost (or *.localhost) is only a warning: it is fine while you develop, but a reviewer refuses it.
  • Fix: serve the app over HTTPS and update the manifest, then eekaam app deploy. For development, eekaam app dev opens a tunnel and uses its public URL on your dev store, so the manifest can stay on localhost until you host the app.

security.embedded-origin

Automated · error

The origin of embedded_url is listed in allowed_origins.

  • Passes: embedded_url = "https://app.example.com/app" and allowed_origins contains "https://app.example.com". Default ports are ignored when comparing. Skipped when there is no embedded_url.
  • Fails: the origin is missing, or differs in scheme, host or port, or embedded_url is not an absolute URL. The admin refuses to load the frame, and the merchant sees a blank page.
  • Fix: add the exact origin — scheme, host and port, no path — to [auth] allowed_origins, and deploy the manifest.

security.no-client-secret

Automated · error

EEKAAM_CLIENT_SECRET is only read in server files.

  • Passes: EEKAAM_CLIENT_SECRET appears only in server-only files. Under app/, src/ and public/, a file counts as server-only when:

    • its name contains .server. (for example app/eekaam.server.ts),
    • it sits in a server, .server or api directory,
    • it is a Next.js route.* or middleware.* file, or
    • it starts with a "use server" directive.

    Files outside app/, src/ and public/ — a standalone server.js, scripts/ — are not bundled for the browser and are never reported.

  • Fails: any other file under app/, src/ or public/ mentions EEKAAM_CLIENT_SECRET. That code can end up in the browser bundle, and anyone can read the secret from it.

  • Fix: move the code that uses the secret into a .server.ts module and call it from a loader or action. If the secret was ever shipped to a browser, rotate it.

security.webhook-signature

Automated · error

If the app declares webhooks, its code verifies the signature.

  • Passes: the app's code calls verifyWebhook, or computes an HMAC over the raw request body to compare with X-Eekaam-Hmac-Sha256. Skipped when the manifest declares no [[webhooks]].
  • Fails: webhooks are declared but nothing in the code verifies them. Anyone who finds the URL could post fake events. eekaam app check only looks for verification code; a reviewer checks that it runs before the body is used.
  • Fix: verify every delivery before parsing it, over the exact bytes received, with a timing-safe comparison. See Webhooks. Apps scaffolded with eekaam app init already do this in app/routes/webhooks.tsx.

security.session-token

Manual

Requests from the frame to the app's backend are authenticated with the session token, not a shop name in the URL.

  • Passes: the frame gets a token from sessionToken() (or the session_token query parameter), sends it with each request, and the backend verifies it with POST /admin/oauth/session/verify before acting.
  • Fails: the backend trusts shop, tenant or host from the query string or a request body to decide which store's data to read or change. Anyone could change that value.
  • Fix: send Authorization: Bearer <session token> from the frame, and resolve the store from the verified token on the server. See sessionToken.

Data rules

data.uninstall

Automated · warning

The app subscribes to app/uninstalled and deletes the store's token and data when it arrives.

  • Passes: [[webhooks]] includes topic = "app/uninstalled", and its handler deletes the access token and the data the app holds for that store.
  • Fails: no app/uninstalled subscription in the manifest. eekaam app check warns; a reviewer also checks that the handler really deletes the data.
  • Fix: subscribe to app/uninstalled, and in the handler stop billing, delete the store's token, and delete or schedule deletion of its data.

data.minimal-scopes

Manual

Every requested scope is used by a feature the listing describes.

  • Passes: each scope in [access] scopes maps to something the listing says the app does. A reviews app asks for read_products, not write_orders.
  • Fails: a scope with no feature behind it, or a write_ scope where read_ is enough.
  • Fix: remove scopes you do not use, then eekaam app deploy. If a scope is needed for a feature the listing does not mention, describe that feature.

Listing rules

listing.complete

Manual

Name, description, icon, screenshots, support and privacy URLs are present and accurate.

  • Passes: the listing says what the app does in plain words, the screenshots show the current app, and the support and privacy URLs load and belong to you.
  • Fails: placeholder text, screenshots of another app or an old version, a missing icon, or support and privacy links that are broken or generic.
  • Fix: complete the listing on the partner dashboard, and check each link from a private browser window before submitting.

Updated 17 September 2026