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
- 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. - 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.
- 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 callsconfirm()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.
| Flag | Default | What it does |
|---|---|---|
--json | off | Print 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:
| Status | Meaning |
|---|---|
pass | The rule holds |
fail | An error: the rule is broken, and submission is blocked |
warn | A warning: reported so you can fix it; it does not block submission |
skip | The 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 directoryeekaam 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 themThe rules
| Id | Kind | Severity |
|---|---|---|
ui.kit | Automated | error |
ui.no-browser-dialogs | Automated | error |
ui.no-top-navigation | Automated | error |
ui.save-bar | Manual | — |
ui.native-overlays | Manual | — |
ui.loading-states | Manual | — |
security.https | Automated | error |
security.embedded-origin | Automated | error |
security.no-client-secret | Automated | error |
security.webhook-signature | Automated | error |
security.session-token | Manual | — |
data.uninstall | Automated | warning |
data.minimal-scopes | Manual | — |
listing.complete | Manual | — |
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 apackage.jsonin the project depends on@eekaam/ui-reactor@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-reactand 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 usetoast(). - Fails: a call to the global
alert(),confirm()orprompt()in the app's source, includingwindow.confirm(),globalThis.confirm()andself.confirm(). Methods with the same name (modal.confirm()) and your own functions calledconfirmare 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. Replacealert()withtoast(), or with an<e-banner>when the message must stay on screen. Replaceprompt()with an<e-modal>holding an<e-text-field>.
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
loadingon<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.tomlis an absolutehttps://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 onlocalhost(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 devopens a tunnel and uses its public URL on your dev store, so the manifest can stay onlocalhostuntil 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"andallowed_originscontains"https://app.example.com". Default ports are ignored when comparing. Skipped when there is noembedded_url. - Fails: the origin is missing, or differs in scheme, host or port, or
embedded_urlis 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_SECRETappears only in server-only files. Underapp/,src/andpublic/, a file counts as server-only when:- its name contains
.server.(for exampleapp/eekaam.server.ts), - it sits in a
server,.serverorapidirectory, - it is a Next.js
route.*ormiddleware.*file, or - it starts with a
"use server"directive.
Files outside
app/,src/andpublic/— a standaloneserver.js,scripts/— are not bundled for the browser and are never reported. - its name contains
-
Fails: any other file under
app/,src/orpublic/mentionsEEKAAM_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.tsmodule 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 withX-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 checkonly 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 initalready do this inapp/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 thesession_tokenquery parameter), sends it with each request, and the backend verifies it withPOST /admin/oauth/session/verifybefore acting. - Fails: the backend trusts
shop,tenantorhostfrom 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]]includestopic = "app/uninstalled", and its handler deletes the access token and the data the app holds for that store. - Fails: no
app/uninstalledsubscription in the manifest.eekaam app checkwarns; 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]scopesmaps to something the listing says the app does. A reviews app asks forread_products, notwrite_orders. - Fails: a scope with no feature behind it, or a
write_scope whereread_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