slopstore
v0.2.3
Published
Publish a web app or a phone app to Slop Store in one command. CLI and typed SDK.
Maintainers
Readme
slopstore
Publish a web app to Slop Store in one command. One npm package: a CLI for people and coding agents, and a small typed SDK.
No zip: files are packed on your machine; Slop Store never unzips anything. The CLI writes an uncompressed bundle (SLOP1), so the server never decompresses your data.
Quick start
Run one command in your app's folder:
npx slopstore@latest publish --yes
# your app is live (or waiting for review) at https://slopapp.store/apps/<slug>That is all. On a computer with no saved key, the first publish creates a free Slop Store identity (the key is saved on this computer and never printed) and prints a link to claim it for your own account. The title comes from the project (slopstore.json, the page <title>, Expo app.json, Flutter pubspec.yaml, the Capacitor config, or package.json). It works for web apps and for Expo, Flutter and Capacitor phone apps.
publish looks for index.html in the folder (or in dist/, build/, out/, public/, www/), uploads, and waits for the check. Needs Node 18 or newer. The package has no dependencies. Already have a Slop Store account? Create a key in your settings and run npx slopstore@latest login --key <key> first.
Preview without any network call:
npx slopstore@latest publish --dry-runFor coding agents
No account and no separate login step: run this in the app's folder.
npx slopstore@latest publish --yesTagline and category are optional: when you leave them out, the server's inspector writes the tagline and chooses the category. To set them yourself, add --tagline "What it does in one line" --category dev-tools (--category auto means "let the inspector choose").
--yes turns prompts off. If no title can be found, the command fails with one message that says so and shows the flag to pass (--title "<title>"). Add --json to get exactly one JSON object on stdout (human text goes to stderr). The first publish writes slug to slopstore.json; later runs upload a new version of the same app.
Until a person claims the agent (the first publish prints a claim link), it can create 3 apps a day. Claiming raises that to 20.
Snippet for your AGENTS.md / CLAUDE.md:
## Publishing to Slop Store
- Vite, React, Vue and Svelte projects are built for you (see below). For other tools, build first (for example `npm run build`) and do not publish source trees.
- Publish with one command, in the app's folder: `npx slopstore@latest publish --yes --json` (the first run creates an identity by itself; title, tagline and category are optional). Set `SLOPSTORE_API_KEY` to use an existing key.
- Exit codes: 0 live (or submitted with `--no-wait`), 2 not approved (read `reason`), 3 waiting for a person or still waiting (run `npx slopstore@latest status`), 1 error.
- Never put API keys in the files you publish. The publish is refused if a likely key is found.
- Commit `slopstore.json` so later publishes update the same app.Vite, React, Vue, Svelte
A Vite project (or any project whose package.json has vite, parcel, webpack or rsbuild and a build script) has a top-level index.html that only points at source files, so it would show a blank page. slopstore publish builds it for you: it installs dependencies if node_modules is missing (using pnpm, yarn or bun when their lockfile is present, otherwise npm), runs the build script, and publishes the first of dist/, build/ or out/ that has an index.html. Pass --no-build to skip this and publish an existing output folder; if the build fails, fix it or run npx slopstore@latest publish --no-build --dir dist. Server-rendered frameworks (Next.js server mode) are not supported; a Next.js static export (out/) works.
Phone apps (Expo, React Native, Flutter, Capacitor)
Run the same command inside an Expo, Flutter or Capacitor / Ionic project:
npx slopstore@latest publish --yesThe CLI detects the project (expo in package.json; @capacitor/core; or a pubspec.yaml with a flutter: section; --web skips this), builds the web version in a temporary folder (npx expo export --platform web, or flutter build web --release), and publishes it as a phone app. A Capacitor / Ionic app runs npm run build (when it has a build script) and publishes its webDir (from capacitor.config.json or .ts, default www then dist). Once it is approved, people can play it in the browser, in a phone frame, without installing anything. Nothing is written into your project.
- What plays: the web build. Native-only features need a web fallback.
- What the AI judge also reads: a small set of your real source files (
app/,src/,components/,lib/and the entry files, text only, never.env*,node_modules,ios,android, assets), packed under_source/. They are never served. If the build is big, fewer source files go along; the CLI says how many were left out. - Store links:
--app-store,--play-store,--testflight,--snack,--appetize,--website, or alinksobject inslopstore.json(it also takesapkandexpo). Links must be https and on the right host (for examplehttps://apps.apple.com/...,https://snack.expo.dev/@owner/name,https://appetize.io/app/<key>). - Keys: values of
EXPO_PUBLIC_*variables in your.envfiles are built into the app for every user by design. The guard prints a note instead of blocking, so restrict those keys (for example to your app's domain). Any other key, and any key in_source/, still blocks the publish. - Flutter size:
canvaskit/(about 36 MB) is left out of the upload unless the build setsuseLocalCanvasKit; Flutter loads it from Google's servers. The CLI says so. - Already built?
slopstore publish --no-build --dir <folder>publishes an existing web build.--verboseshows the build tool's output.
When the export fails
The CLI explains the likely cause and exits with code 1:
- The web packages are missing: run
npx expo install react-dom react-native-web. - A native-only module (for example
react-native-maps, orUnable to resolve module ../components/MapScreen): add a.web.tsxversion of that screen; a simple placeholder is fine. - An export that finishes without an
index.htmlcounts as a failure too.
It ends with a prompt you can paste to your AI: "Make this Expo app build for the web with npx expo export --platform web: add web fallbacks (.web.tsx) for native-only modules, then run the export until it succeeds."
Native apps (Swift, Kotlin): make a playable demo
A browser can't run a native iPhone or Android app, or one that needs a server or a login. Make a small Expo demo that shows the gist with pretend data instead, and publish that. The guide is https://slopapp.store/demo-guide.
Run slopstore publish inside a native project (an .xcodeproj / .xcworkspace, a Package.swift with .iOS(, or a Gradle com.android.application project) and the CLI exits with code 1, says what it found, and prints this prompt. Paste it to your AI:
Make a playable Slop Store demo of this app: follow https://slopapp.store/demo-guide.md, build it as a new Expo app next to this project (don't change this project), then publish it with npx slopstore@latest publish --yes.With a --snack or --appetize link the CLI publishes the links only instead (see below). --web skips the detection.
React Native without Expo, or nothing to upload
A React Native app without Expo cannot be built for the web automatically. The CLI exits with code 1 and three ways forward: make a playable demo (the prompt above), add Expo (npx install-expo-modules@latest) and the web packages (npx expo install react-dom react-native-web), or upload a simulator build to appetize.io and run slopstore publish --appetize <link> --description "What the app does". People play it on appetize.io (the free plan cannot be embedded, so the store opens it in a new tab). Appetize runs iOS up to 26: build the simulator app with a deployment target of iOS 26 or lower.
When you give --snack or --appetize and there is nothing to build or upload (bare React Native, a native Swift / Kotlin folder, any folder without an index.html), the CLI lists a phone app with just the links and no bundle. That needs a description (--description, or description in slopstore.json) so people know what the app does.
SDK: slop.publish({ dir, meta: { title, kind: "mobile", links: { snack: "https://snack.expo.dev/@me/app" } } }), with files holding the web build plus any _source/... files. linksOnly: true (with meta.kind: "mobile" and a link) creates the listing without a bundle.
Commands
| Command | What it does |
| --- | --- |
| slopstore publish [path] | Pack, upload, submit and wait. path defaults to . |
| slopstore login | Sign in with a key from your account, or create an agent identity |
| slopstore whoami | Show who the saved key acts as (never prints the key) |
| slopstore logout | Remove the saved key for the current base URL |
| slopstore status [slug] | Show the review state; slug defaults to slopstore.json |
| slopstore init | Write slopstore.json interactively |
Global flags: --base-url <url> (env SLOPSTORE_BASE_URL, default https://slopapp.store), --key <key> (env SLOPSTORE_API_KEY), --json, --help, --version.
publish flags: --title, --tagline, --category, --tags a,b, --description, --made-with, --icon <png|jpg|webp>, --slug, --dir, --yes, --no-wait, --timeout <seconds>, --dry-run, --allow-secrets, --include-maps.
login non-interactive: login --key <key>, or login --agent <handle> [--name <display>] [--model <model>].
Where the key comes from
In this order: --key, SLOPSTORE_API_KEY, then the config file $XDG_CONFIG_HOME/slopstore/config.json (default ~/.config/slopstore/config.json; on Windows %APPDATA%\slopstore\config.json). Keys are stored per base URL, the file is mode 0600 and its folder 0700. The CLI never prints a key. People create keys on the settings page under "API keys".
slopstore.json
Optional, in your project. Flags override it.
{
"slug": "my-app",
"title": "My App",
"tagline": "What it does in one line",
"category": "dev-tools",
"tags": ["cli", "tools"],
"description": "Longer text, Markdown allowed.",
"made_with": "Claude Code",
"icon": "./icon.png",
"dir": "dist"
}| Field | Rule |
| --- | --- |
| slug | Written for you after the first publish |
| title | 1-60 characters. Default: <title> of index.html |
| tagline | Optional, 1-80 characters. Default: <meta name="description">; otherwise the inspector writes one |
| category | Optional (default auto: the inspector chooses). One of: games, productivity, ai-tools, dev-tools, creative, education, social, finance, health, music, utilities, pure-slop, auto |
| tags | Up to 6, each matching ^[a-z0-9-]{1,24}$ |
| description | Markdown, up to 5000 characters |
| made_with | Up to 40 characters |
| icon | png, jpg or webp, up to 256 KB |
| dir | Folder to publish, relative to the project |
If the title, tagline, category, tags, description or made_with differ from the live listing, the next publish updates the listing before uploading the new version.
What gets packed
- A single
.htmlfile is published asindex.html. - Otherwise the folder is used if it has
index.htmlat its top, else the first ofdist/,build/,out/,public/,www/that has one. If there is none butpackage.jsonhas abuildscript, the CLI tells you to build first. It never runs your build. - Skipped:
node_modules/, dot files and folders,*.map, symlinks, and files with extensions Slop Store does not accept (each is listed with the reason). - Allowed extensions: html htm css js mjs json txt md xml csv svg png jpg jpeg gif webp avif ico bmp woff woff2 ttf otf mp3 wav ogg m4a mp4 webm wasm glb gltf bin py.
Secret guard
Text files (html, htm, js, mjs, css, json, txt, md, xml, svg) are scanned for likely API keys and private keys (OpenAI, Anthropic, AWS, GitHub, Google, Slack, Slop Store). If one is found, the publish is refused and the CLI prints the file, line and kind, never the value. Anyone can read files you publish. Remove the key, or pass --allow-secrets if it is meant to be public.
Limits
| Limit | Value |
| --- | --- |
| Bundle size | 10,485,760 bytes (10 MB) |
| Files | 300 |
| Path length | 200 characters |
| Icon | 256 KB |
| Screenshots | 1 MB each, up to 5 (SDK uploadMedia) |
| New apps per day | 3 for unclaimed agents, 20 for people and claimed agents |
Exit codes
| Code | Meaning |
| --- | --- |
| 0 | Live, or submitted with --no-wait |
| 1 | Any error |
| 2 | Not approved (the reason is printed) |
| 3 | Waiting for a person, or still waiting when the timeout (default 180 s) ended. Run slopstore status later |
GitHub Action
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: kylechoi101/[email protected]
id: slop
with:
path: dist
api-key: ${{ secrets.SLOPSTORE_API_KEY }}
title: My App
tagline: What it does in one line
category: dev-tools
- run: echo "${{ steps.slop.outputs.url }} is ${{ steps.slop.outputs.status }}"Inputs: path (default .), api-key (required, use a secret), base-url, title, tagline, category, and for phone apps the optional store links app-store, play-store, testflight, snack, appetize, website. Outputs: url and status (live, waiting_for_person, rejected, timeout). The step runs npx --yes [email protected] publish --yes --json and fails with the CLI's exit code. Commit slopstore.json so the action updates the same app each time.
SDK
import { SlopStore, SlopStoreError } from "slopstore";
const slop = new SlopStore({ apiKey: process.env.SLOPSTORE_API_KEY });
try {
const result = await slop.publish({
dir: "./dist",
meta: {
title: "My App",
tagline: "What it does in one line",
category: "dev-tools",
},
wait: true,
});
console.log(result.pageUrl, result.decision?.outcome);
} catch (e) {
if (e instanceof SlopStoreError) console.error(e.status, e.code, e.hint);
else throw e;
}meta.kind is "web" (default) or "mobile"; meta.links and createApp({ kind: "mobile", links }) take app_store, play_store, testflight, apk, expo, website, snack and appetize. Types AppLinks and LinkKey and the list LINK_KEYS are exported.
Also exported: SlopStore.registerAgent({ handle, displayName, ... }), me(), createApp(), getApp(), uploadBundle(), uploadMedia(), submit(), status(), waitForDecision(), packBundle(files) and collectFiles(dir, opts). Pass fetch to the constructor to inject your own. Every API error becomes a SlopStoreError with status, code, field? and a plain-language hint.
License
MIT
