Kuratowski is a demo for now. Connecting apps with a sentence is not open yet.

Review Guidelines

Last updated: 2026-10-24

These guidelines list what an app has to meet to be listed in the marketplace. Every rejection and every change request cites at least one guideline number, so a developer reads the basis of the decision instead of guessing it.

1. Security and data

1.1 Undeclared data is not carried

Your app may only send and accept the fields it declares in its manifest. A connection carrying an undeclared field goes outside what the user consented to.

1.2 Sensitive data is justified

Ports carrying identity, health, financial or biometric data are declared as such. A sensitive port with no written justification is rejected; the platform does not accept matches of this kind without a reason.

1.3 Your code does not run on the platform

Your app runs on your own infrastructure. The platform only calls the HTTPS endpoints you declare; you cannot send us code, scripts or packages to be executed.

1.4 Credentials are not reused

A token issued for one user cannot be used for another user, for another app, or for your own internal work.

1.5 No real personal data in media

Your screenshots and videos cannot show the name, email address, phone number, address, invoice or health data of your real customers. Use fictional sample data. The storefront is public, and a record that lands there has been published without that person's consent.

1.6 Records carry their private mark

Every record leaving a shared business object (`business/…`) port carries the `_private` field as a boolean, and it is `true` on records the user marked private. The mark is placed on a person: when a customer is marked private, the sales, appointment and payment records attached to them come out marked as well. A delivery that does not carry the mark is rejected on the platform side; an app that never computes the mark violates this guideline. You cannot choose NOT TO SEND a marked record: the platform does the cutting, you compute the mark and carry it. How the mark travels from a person to a record is written in guideline 1.10, and that is the mechanism this guideline rests on.

1.7 Fields are classified

Business object ports declare their fields inside `shape` and write the class of each field. Fields counted as the inside of a business (purchase price, cost, margin) are `internal`, and fields a person writes in their own words (note, description) are `free-text`; neither passes through a connection unless the user explicitly asks for it. The class in the dictionary is a FLOOR: a developer may pull a field into a more closed class, never into a more open one. Because both ends are wired to a machine, a moderator does not need to know the dictionary by heart; a declaration more open than the dictionary drops the submission at the schema, and even if it does not drop, the dictionary's class is applied again at delivery.

1.8 Filtering applies on every surface data leaves the user through

On connections the platform builds, the platform does the cutting: an output port carrying records declares a schema and fields are stripped by it. This guideline covers the developer's OWN surfaces. Filtering private-marked records and fields in the `internal` / `free-text` classes applies not only on connections the platform builds, but on every surface data LEAVES the user through: read endpoints you open for add-ons, exports, and anything reachable with a key the user handed to a third party. The user's OWN screen is outside this and has to be: a person sees their own purchase price and their own note, and putting a filter there would mean hiding the user's data from the user. The distinction disappears silently in exactly one place: if the SAME identity resolver accepts both your panel session and the key you hand out, every panel endpoint turns into an externally reachable surface without anyone deciding so. The two identities are resolved separately, and a key issued to the outside never reaches panel endpoints.

1.9 Writes arriving from outside are validated as records

You cannot accept a write arriving from a connection or from an endpoint you opened yourself as it is. At least these three apply: data that will open a new record has to carry the field identifying that record (a name, for example); data that arrives without an identity and repeats the same record within a short window does not open a second record, it is answered with the identity of the existing one; there is a body limit rejected without being read. **The only exception to the identifying field requirement is a `business/party` record carrying the private mark:** that record is not a business record but an instruction about a person, and it carries no name because the platform reduces it to an identity (guideline 1.10). When a mark arrives for an identity you do not know, you store the mark but do not open a nameless record in the user's list. A rejected record does not drop the whole batch, it is reported one by one. The sender being authenticated does not remove this validation: the damage forms in the user's books, and a faulty integration dirties those books faster than an anonymous bot.

1.10 The party port carries the mark and applies an incoming mark

The private mark travels between apps together with the person's record, and the `business/party` port is the only way it travels. Both directions bind you. As the SENDER: when you mark a person, you do not pull them out of the port, you send the record with `_private: true`; the platform reduces it to an identity (`party_id` and the flag) and the name, phone number and address are not delivered. When the mark is lifted, the record goes in its normal form with `_private: false`. As the RECEIVER: when you receive a party record carrying `_private: true`, you have to mark YOUR OWN records attached to that identity; when `false` arrives, you remove the mark. An app that ignores an incoming mark silently voids a decision the user made in another app. **When a mark arrives for an identity you do not know:** you store the mark against that identity, but you do not open a nameless card in the user's list. The "identifying field" requirement of guideline 1.9 does not apply here, and it is written there too; until a name arrives there is no record to show, only a mark to remember.

1.11 A development identity cannot stay open in production

Your identity module may and should have a development branch: a fixed test user opened by an environment variable. But that branch has to be CLOSED in production, and what closes it must be code, not a document: if the app sees that variable while running in production, IT MUST NOT START. A test identity left open turns every panel endpoint into an unauthenticated surface; the whole of the user's books (internal fields, notes, key issuing, data deletion) opens to anyone who finds the address. The same requirement holds for every setting that bypasses authentication: a fixed token, a flag that turns verification off, a "public" test branch. The measure is one sentence: if this variable is carried into the production environment by accident, the app must fail to start.

1.12 A record carried in an opaque field is justified

The INSIDE of a field you declare as `json` or `binary` is unreadable for the platform: the user's private mark and the field classes can only be applied inside that field to a limited degree. So you declare every field you are able to declare; putting a whole record into a single opaque field means appearing to meet the requirement of guideline 1.1 while switching the cutting off. Opaque fields are NOT forbidden and will not be: in a relay the body passes through as it came from the source, in a polymorphic stream every event has a different body, and you cannot know the names of fields the user defined themselves either. During review these ports are flagged and you are asked why you could not declare the fields. Your answer is written into the submission thread; a submission leaving declarable fields opaque is asked for changes.

2. Technical requirements

2.1 The service has to be running

Every endpoint you declare must be callable. Your endpoints are probed during review, and an unreachable endpoint stops the review from finishing. Unless your app is suspended, its endpoints keep being probed at least once a day while it is in the store or installed by anyone, and when one fails two probes in a row we email you.

2.2 HTTPS is required

All endpoints are served over HTTPS with a valid certificate. Plain HTTP is not accepted.

2.3 The manifest matches the schema and the slug is permanent

The manifest has to match the published schema. Once approved, the slug does not change: users' installations, connections and the addresses they shared depend on it.

2.4 The app has to open inside the frame

Your app page opens inside the platform's frame. If your `X-Frame-Options` or `Content-Security-Policy` headers prevent this, the app cannot be used.

2.5 Access is given for review

An app that requires signing in provides, in its review information, the details of a demo account that does not expire. Without access the review can only be done on the manifest, and the app cannot be approved.

2.6 App or add-on is declared truthfully

The `listingKind` field in the manifest has to tell the truth. The measure is one sentence: is it useful with no connections at all? If it is, it is an app; if it is not, it is an add-on. Your declaration is tested during review by installing and opening it; a submission declaring a different kind than it actually is is asked for changes. An add-on has to have at least one input port: something that consumes nothing is attached to nothing.

2.7 An add-on does not connect to an add-on

There is always an app above an add-on. You can attach to more than one app, but you cannot build a chain; a connection joining two add-ons cannot be created. If you need to chain two capabilities, either combine them in a single add-on or apply as an app.

2.8 Embedded session rules are followed

When your app verifies an identity token it pins the algorithm, the issuer and the audience; it refuses a second session opened with the same **panel token** (replay rejection is a rule of the panel token, while a connection token arrives with the same `jti` on every delivery); it drops the token from the address bar as soon as the panel opens, and it sends `Referrer-Policy: no-referrer` on the panel page. These four are the only layer protecting the user's identity, and the platform cannot apply them on your behalf.

2.9 The dictionary is used for shared business objects

If the data you carry exists in the shared business object dictionary (party, product, stock level, stock movement, sales document, money movement, appointment, free time slot), you use that type and those field names. You are free to split more finely and to add fields that are not in the dictionary; you cannot give your own name to something shared. The gate is honesty: what your port returns has to be what you declared. How many ports you declare and how finely you split them is not a reason for rejection, it is a grade shown on your store page.

2.10 A declared input port accepts the probe call

During review every input port you declare is called once for real, within the time limit of one review run: the canonical example we would send on the wire for that port type arrives at your adapter with the `Authorization` header a normal delivery carries. The call carries the `x-kuratowski-test: true` header and the values inside it are fictional (`example.com`, `+1 555 01xx`); you do not have to process the record, you can see that header and drop it. If your adapter returns an error to this call the submission cannot be approved, because every connection a user would build would die with the same error. An endpoint that is unreachable or does not recognise our token is not rejected by this guideline; the only case rejected is an adapter that runs and refuses the type it declared itself. The run has a time limit: if it runs out before we reach a port, that port is not called at all, nothing about it is measured, and the moderator sees which ports were left uncalled. A port we could not call does not block approval on its own, so a slow endpoint is not a rejection either.

2.11 Frame permissions are declared and each one is used

Your panel is opened inside a sandboxed frame. Two permissions are always given and are not declared by anyone: running scripts, and keeping your own origin. Everything above that baseline (`downloads`, `popups`, `forms`) is given only if your manifest declares it in `frame.capabilities`, and is off by default. Declare only what a user can actually do in your panel: an export button for `downloads`, a link that opens in a new tab for `popups`, a real form submit for `forms`. A permission declared without such an action is refused, because it widens the sandbox around your code for nothing. Nothing outside that closed set can be asked for at all: the schema refuses `allow-top-navigation`, `allow-modals`, `allow-popups-to-escape-sandbox` and the rest at submission time, and no review can grant them. A new version that ADDS a permission is looked at again, because what was approved was the panel as it was then. Removing a permission needs no review. If you do not declare `downloads` the browser blocks the download SILENTLY, with no error for you and none for the user, so a missing declaration looks exactly like a broken export button.

2.12 An add-on has a screen

An add-on opens as a panel next to the app it attaches to, so it has to have a screen: declare `embeddablePanelUrl`. This holds even when the add-on's effect shows up inside the app it attaches to; the panel is where the user sees what the add-on does for them. A new add-on, or a new version of one, without a panel URL is refused at submission time.

3. Storefront content

3.1 The description has to be accurate

The name, description and screenshots describe the work the app actually does. Content describing a feature that does not exist is rejected.

3.2 No paid app is listed during the free period

The platform does not process payments. An app that charges the user through the platform cannot be listed.

3.3 Trademarks and copyright

You cannot use a trademark, logo, screenshot or text you do not own. You cannot choose a name evoking another product when you have no connection to it.

3.4 Placeholder content is not accepted

"Coming soon" screens, empty screenshots and sample text cannot go live. Storefront content has to be finished.

3.5 No steering to buy outside the storefront

The address of your developer site is shown on your product page and is not removed; the user has to be able to see the company behind it. But your storefront text, your screenshots and your in-app screens cannot invite the user to buy the same app outside the platform, to sign up there, or to set it up there. The address you give is for information and support, not a second sales channel.

3.6 A privacy policy address is given

Your store page has to carry the `https` address of the privacy policy you publish yourself, and that address has to keep working. The text has to state clearly what data your app collects, how it collects it and what it uses it for. The user reads this BEFORE installing: installing means starting to pour their data into your system.

3.7 A support path stays open

Your store page carries a support address the user can reach you at, and that address is read. You, not the platform, are the first point of contact for a problem between a user and a developer; a developer who cannot be reached means the user has no option left but to uninstall the app.

3.8 Screenshots show the product running

Screenshots show the real screens of the app; they cannot be only a logo, a sign-in screen, a splash image or a marketing poster. If you provide a video, it too has to be a real recording of the product, not a promo film.

3.9 The category is chosen correctly

The category in the manifest has to describe the product's main job; a category chosen in order to sit in a more crowded bucket is corrected. The category says what work you do, not how you work.

3.10 Content is suitable for every age

The name, icon, screenshots, video and description have to be content a visitor of any age can see; the storefront is public and is browsed without signing in. Content praising violence, sexual content, hate speech or illegal activity is not listed.

3.11 Copies of the same product are not listed

Submitting the same product several times with small differences (a separate record per customer, per city or per colour) pollutes the catalogue and breaks discovery. If you offer different configurations to different customers, do it inside a single app.

3.12 Translations describe the same product

You can publish your store page in every language of the site. Every translation has to describe the same product, the same features and the same limits: a feature promised in one language and not promised in another is a false statement for the user who speaks that language. In a language you do not translate, your primary text is shown; a missing translation is not a violation, a WRONG translation is.

3.13 The primary text of a listing is written in English

You write your store page's main text (tagline and full description) in English, and you state which language you wrote it in. Translations are optional: add the languages you want and leave the rest empty. The reason is what the visitor sees: everyone without a matching translation reads the main text, so the main text has to be in the site's common language. A listing with no main text, or with main text that is not English, is sent to review. Leaving the full description empty in the main text while filling it in only for a translation has the same result: in that field, a visitor in another language sees nothing. When the tagline is empty the visitor reads your manifest's short description instead, so that sentence is judged as your listing's main text and it has to be English too.

3.14 The platform's name is reserved

Kuratowski is the name of this platform and of the apps it publishes itself. No developer name, app name or slug can contain it, and neither can a spelling made to look like it: look-alike letters from other scripts, accents, spaces or punctuation inside it, or one changed, added, missing or swapped letter. The check runs when you apply, when you create an app, when you submit a version and again at approval; a name that fails it is refused with `reserved_name`. Saying in your description that your app works with Kuratowski is fine: this guideline covers names only.

3.15 A developer name belongs to one developer

Two developers cannot share a name, so a user can always tell who made an app. A name counts as the same when it only differs in letter case, accents, spaces or punctuation, or uses look-alike letters from another script. The name is held from the moment you apply; if another developer already holds it, your application is refused with `display_name_taken`. Once you are approved, your developer name cannot be changed from the application form.

4. Reviews and ratings

4.1 Rating manipulation is forbidden

You cannot offer incentives in exchange for a review or a rating, write ratings with fake accounts, or pressure a user into changing a negative review.

4.2 No personal data in replies

In the reply you write to a review you cannot share information about the user's account, email address or usage records.

5. Process

5.1 Every negative decision rests on a guideline

Every submission that is rejected or asked for changes carries at least one guideline number and a free-text reason. No decision is made without a number.

5.2 Resubmission

A submission that was asked for changes can be corrected and sent again. The review starts from the beginning.

5.3 Suspension

A published app can be suspended if it later turns out to violate one of these guidelines. The suspension is shown in the developer console together with its reason.

5.4 A published page keeps being reviewed

Your store page goes live without waiting for review, but it is reviewed after publication: every change you make lands in the moderator's queue, and each language is read separately. Users can report an app as well. If a violation is found as a result of that review, guideline 5.3 is applied.