Keou Studio documentation
Everything needed to install, configure, deploy, secure and extend the studio. Written for someone who has never seen the codebase.
What Keou is
Keou turns one raw product photograph into a finished commercial campaign: images, cinematic video, voice-over, sound design, and every delivery format your channels need. It is a full studio, not a wrapper around a generation endpoint.
The thing that makes it useful for product work is that the product itself is pixel-locked. The scene around it is generated; the shape, text, labels and logos on the object are not redrawn. That is the difference between a usable packshot and an image that merely resembles your product.
Everything ships in the public repository. Polish, remix, format adaptation, export packs, voice, sound, upscaling, teams, sharing — and the production prompt stack that makes the output good. There is no paid tier holding features back.
Two files stay private, and neither is a feature: our provider costs and margin table, and the Stripe plumbing that bills our hosted instance. Nothing in the studio reads them.
Three ways to run it
Hosted, no account
Open studio.kanaky.xyz/launch, paste your provider key, produce. Creations join a public gallery and carry a watermark.
Hosted, free account
Same studio, but your work stays private, you get a library, downloads, projects and share links.
Self-hosted
Your servers, your database, your storage, your brand. Identical feature set. This documentation is mostly about this path.
What it does
Product images
A commercial scene built around your photograph. The object stays untouched.
Cinematic video
Clips from a still. Grok Imagine, Kling 2.6 / 3.0, Seedance 2, Veo 3.
Polish
Retouch a render without regenerating it — lighting, blemishes, background.
Remix
Same product, new creative direction, in one action.
Format adapt
Every aspect ratio your channels need — recomposed, not cropped.
Export packs
Platform-ready variants from one approved visual, in one action.
Voice & sound
Voice-overs and sound effects, in the same pipeline as the visuals.
Upscaling
Images and video to 8K.
Clients & campaigns
Output organised by client, campaign and approval state.
Share links
Client review links that collect structured feedback.
Teams
Accounts, roles and quotas for a studio of more than one.
Assistant
A chat agent that drives the whole toolset. Needs your own Anthropic key.
Install
What you need
| Requirement | Notes |
|---|---|
| Node.js required | Version 20 or later. |
| PostgreSQL required | Any recent version. Migrations run automatically at first boot — you do not create tables by hand. |
| Provider key required | A KIE.AI or Fal.ai account. Each user pastes their own in the studio — the server does not need one. |
| Cloudflare R2 required | Free tier is enough. Without it every upload returns 503 — and uploading a photo is the first thing the studio does. It also keeps results alive past the ~14 days a provider URL lasts. |
From source
terminal# clone and install
git clone https://github.com/kanakytech/keou.git
cd keou
npm ci
cp .env.example .env
# four values matter, not two
DATABASE_URL=postgresql://…
JWT_SECRET=… # openssl rand -hex 32
ADMIN_EMAIL=you@example.com # seeds your account on first boot
ADMIN_PASSWORD=… # 12+ chars
npm start
Set ADMIN_EMAIL and ADMIN_PASSWORD before the first boot. Public self-serve signup is off in a self-hosted deployment, on purpose — so without a seeded account the instance comes up with no way to sign in, and you have to drop the database to try again.
Open http://localhost:3401 and sign in. Migrations run on boot.
With Docker
terminaldocker build -t keou .
docker run -p 3401:3401 --env-file .env keou
The image is multi-stage, runs as a non-root user, and exposes a healthcheck on /health. It sets NODE_ENV=production, which turns Postgres TLS on — add DATABASE_SSL=0 to your .env if your database is local and speaks plain TCP.
On Railway
Create the project
New project → Deploy from GitHub → this repository, or your fork of it.
Add PostgreSQL
Add a PostgreSQL service in the same project. Railway injects
DATABASE_URLfor you — do not set it by hand.Set the variables
JWT_SECRET, and the fourR2_*values if you are using storage.Deploy
railway.jsonand the healthcheck are already in the repository. Enable auto-deploy onmainto receive upstream updates automatically.
Your first visual
Get a provider key
Sign up at kie.ai — they give you free credits. Copy the long API key string.
Paste it in the key bar
Top of the studio. Press Save. It is written to this browser's
localStorageand nowhere else.Drop a product photo
Any reasonably lit shot works. A plain background helps but is not required — the scene gets rebuilt anyway.
Describe the scene, not the product
Say where it sits and how it is lit: "on wet volcanic stone at dawn, sea mist behind, low warm sun from the left". Do not describe the object — it is already in the photo and it will not be redrawn.
Generate, then refine
Use Polish to fix a detail, Remix for a different direction, Adapt for other ratios, Pack for the multi-format set.
Affiliate disclosure. The kie.ai links on this site carry our referral code. If you sign up through one, KIE pays us a small share of what you spend in your first month. Your price is exactly the same either way, and the platform works with any key — including one created without our link. It is one of the ways an open-source project with no paid tier pays for itself, and we would rather say so than have you find out.
Prompting note. The production prompt stack already handles product fidelity, composition and commercial framing. Your brief should add context, not instructions — the more you fight the stack with technical directives, the worse the output gets.
Architecture
An Express server, a PostgreSQL database, an object store, and a background poller. No queue broker, no microservices, no build step for the frontend — the pages are plain HTML and vanilla JavaScript served statically.
What happens during a generation
Where things live in the repository
| Path | What it holds |
|---|---|
index.js | Entry point. Pins the edition, then loads the server. |
server.js | Express setup: security headers, rate limits, route mounting, boot. |
src/config.js | Every environment variable, read in one place. |
src/migrate.js | Schema migrations. Run automatically at boot. |
src/poller.js | Background completion of asynchronous provider jobs. |
src/routes/ | One file per API surface. |
src/lib/keou-actions.js | The generation engine — validation, persistence, provider dispatch. |
src/lib/studio-prompts.js | The production prompt stack. |
src/lib/providers/ | KIE.AI and Fal.ai adapters behind one interface. |
public/ | The whole frontend. No build step. |
Configuration
Copy .env.example to .env. Two values are mandatory; everything else has a working default.
Required
| Variable | Notes |
|---|---|
DATABASE_URL | PostgreSQL connection string. Migrations run automatically at boot. |
JWT_SECRET | Session signing secret, 16 characters minimum. Generate with openssl rand -hex 32. |
Storage — required to upload
| Variable | Notes |
|---|---|
R2_ACCOUNT_ID | Cloudflare account identifier. |
R2_ACCESS_KEYR2_SECRET_KEY | R2 API token pair. |
R2_BUCKET | Bucket name. Defaults to keou. |
R2_PUBLIC_URL | Public bucket domain, e.g. https://assets.example.com. |
Storage is not optional in practice. Without the four R2_* variables, POST /api/upload answers 503 and names what is missing — and since every generation starts from an uploaded photo, nothing in the studio works. Configure it before your first login. The free tier is enough.
Optional
| Variable | Default | Notes |
|---|---|---|
PORT | 3401 | HTTP port. |
EDITION | opensource | Set by index.js. Leave it alone. |
DEFAULT_PROVIDER | kie | kie or fal. |
KIE_API_KEYFAL_API_KEY | — | A server-side fallback key, for a private instance where you would rather not hand a key to each user. Leave unset for pure bring-your-own-key. |
ANTHROPIC_API_KEY | — | Enables the assistant. Without it the chat returns a clear error and nothing else is affected. |
ANTHROPIC_MODEL | claude-sonnet-5 | Model backing the assistant. |
OPENAI_API_KEY | — | Speech-to-text for voice input in the assistant. |
ADMIN_EMAILADMIN_PASSWORD | — | Effectively required. Seeds your account when the database comes up empty. Without them a self-hosted instance has no way to sign in — public self-serve signup is off by default on purpose. |
DATABASE_SSL | — | Set to 0 to disable Postgres TLS. The Docker image sets NODE_ENV=production, which turns TLS on; a local database speaking plain TCP needs this. |
AGENCY_NAME | Keou | Shown throughout the interface. |
AGENCY_IMAGE_QUOTAAGENCY_VIDEO_QUOTA | 999999 | Internal counters only. There is no billing in this edition — leave them high. |
JWT_EXPIRESREFRESH_EXPIRES | 15m / 30d | Token lifetimes. |
DATABASE_SSL_STRICTDATABASE_CA | — | Certificate pinning for managed PostgreSQL. |
DISABLE_POLLER | — | Set to 1 on every replica but one, so a single instance polls providers. |
Running it in production
Before you expose it
- Set a real
JWT_SECRET— 32 bytes of randomness, not a word. - Terminate TLS in front — the app trusts one proxy hop (
trust proxy: 1), which is what rate limiting and secure cookies rely on. Put exactly one reverse proxy in front, no more. - Set
ADMIN_EMAILandADMIN_PASSWORDfor the first boot, then change the password and remove them. - Configure storage. See the warning above.
Scaling to more than one instance
The app is stateless apart from PostgreSQL and R2, so you can run several replicas behind a load balancer. One thing does not parallelise: the poller.
Set DISABLE_POLLER=1 on every replica except one. Several pollers racing on the same jobs will duplicate provider calls and burn credits.
Staying up to date
The public repository is rebuilt from the Keou core on every change — engine improvements, provider updates and security fixes land as commits on main.
main
Self-host: git pull && npm ci, restart
Keys & security
Where your provider key lives
In your browser's localStorage, and nowhere else. Each generation request carries it in an X-Provider-Key header; the server uses it for that single provider call and never writes it to disk, to the database, or to logs.
The trade-off, stated plainly. Because the key exists only inside a request, the background poller has no key of its own in a bring-your-own-key deployment — it cannot finish a job on your behalf. Your browser drives the polling and the result is stored the moment it lands. Close the tab mid-generation and the job stays pending until you return, and the provider's own URL expires after about fourteen days.
Want the server to complete jobs unattended? Set a server-side KIE_API_KEY or FAL_API_KEY. The poller then works normally — at the cost of the guarantee above. That choice is yours, and it is the reason the guarantee is real rather than marketing.
This is worth verifying rather than believing. Grep the source for X-Provider-Key — you will find it read into a per-request context and passed straight to the provider adapter. That is the whole path.
Account security
| Mechanism | How it works |
|---|---|
| Password storage | bcrypt hashes. Plaintext is never stored. |
| Sessions | Short-lived JWT access token plus a refresh token. Lifetimes are configurable. |
| API keys | Format keou_<32 hex>, 128 bits of entropy. The plaintext is shown once at creation; only the SHA-256 hash is stored. Ten per user. |
| Share links | 128-bit random token, expiry capped at 90 days, optional access code. |
| Rate limiting | Per-route. Auth is strict (15/min), generation is polling-tolerant, uploads support batch work. |
| Headers | Helmet, with a content security policy that includes your R2 domain automatically. |
| Outbound URLs | User-supplied URLs are checked against private and link-local ranges before reaching a provider; the download proxy uses a host allowlist over HTTPS only. |
| Prompt filtering | Requests for sexual content, minors, violence, hate, real people and identity documents are refused before reaching a provider. |
Programmatic access
terminal# 1 — sign in
TOKEN=$(curl -s -X POST https://your-host/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"…"}' | jq -r .accessToken)
# 2 — mint a long-lived key (shown once)
curl -s -X POST https://your-host/api/keys \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"label":"ci-pipeline"}'
HTTP API
Everything is under /api. The generation routes sit directly on /api — /api/video, not /api/generate/video. Authenticated routes accept Authorization: Bearer <token> — either a session token from /api/auth/login or a long-lived key from /api/keys.
Generation
| Method | Path | Does |
|---|---|---|
| POST | /api/generate | Image from a source photo and a brief. |
| POST | /api/video | Video from a still. |
| POST | /api/polish | Retouch an existing render. |
| POST | /api/remix | Re-imagine in a new direction. |
| POST | /api/adapt | Recompose to other aspect ratios. |
| POST | /api/upscale | Upscale a render. |
| GET | /api/status/:type/:taskId | Poll a running job. |
| GET | /api/packs | Available pack definitions. |
| POST | /api/pack | Start an export pack. |
| GET | /api/pack/:packId/status | Progress of a running pack. |
| GET | /api/pack/:packId/zip | Download the finished pack. |
| GET | /api/pending | Your unfinished jobs — used to resume after a reload. |
| POST | /api/report-failure | Report a generation that came back wrong. |
Content and organisation
| Route | Does |
|---|---|
/api/upload | Source images and video. |
/api/download | Signed download of a result. |
/api/history | Your library — list, tag, bulk-move, delete. |
/api/projects | Clients. |
/api/campaigns | Campaigns within a client. |
/api/share | Review links and structured client feedback. |
/api/tools | Voice-over, sound effects, image and video upscaling. |
Account and team
| Route | Does |
|---|---|
/api/auth | Register, login, refresh, logout, change password. |
/api/profile | Read and update your profile. |
/api/keys | Long-lived API keys for scripts and MCP clients. |
/api/team | Invite members, set roles, reset, remove. |
/api/admin | Instance settings, quotas, branding. Admin only. |
/api/analytics | ROI, clients, velocity, campaigns, savings. Admin only. |
/api/activity | Instance activity log. Admin only. |
/api/dashboard | Dashboard aggregates. |
A generation, end to end
terminal# upload a source image (field name is "image")
curl -s -X POST https://your-host/api/upload \
-H "Authorization: Bearer $TOKEN" \
-F "image=@bottle.jpg"
# generate — your provider key rides the request, it is never stored
curl -s -X POST https://your-host/api/generate \
-H "Authorization: Bearer $TOKEN" \
-H "X-Provider-Key: $KIE_KEY" \
-H 'Content-Type: application/json' \
-d '{"imgUrl":"…","creativeDirection":"on wet volcanic stone at dawn"}'
# poll until it lands — pass generationId so the result is persisted
curl -s "https://your-host/api/status/image/$TASK?generationId=$GEN" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Provider-Key: $KIE_KEY"
Pass generationId when polling. Without it the call still returns the result, but the row is never marked complete and the image is never copied to durable storage — you get a provider URL that expires.
The assistant
A chat agent with the entire toolset wired in as tools: it can generate, polish, remix, adapt, build packs, produce voice and sound, and file the results under the right client — from a sentence.
It runs on your own Anthropic key, set either as ANTHROPIC_API_KEY or from Settings → API Keys inside the app. Without a key the chat returns a clear error and nothing else is affected.
| Method | Path | Does |
|---|---|---|
| POST | /api/jarvis/chat | Streaming agent loop with the full toolset. |
| POST | /api/jarvis/stt | Speech to text. Needs OPENAI_API_KEY. |
| POST | /api/jarvis/tts | Text to speech. |
| GET | /api/conversations | Chat history — list, create, rename, delete. |
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Boot fails on migration role … does not exist | DATABASE_URL points at a reachable server but with wrong credentials or a database that does not exist. Create the database and the role first; Keou creates tables, not databases. |
| Uploads fail | Storage is not configured. Set the four R2_* variables. |
| History shows a result, the image is gone | The result was never copied to storage, so the row points at the provider's temporary URL — those expire after about 14 days. Configure the R2_* variables; new results are then copied the moment they land. |
| Generation returns 401 or 403 | The provider rejected the key. Check it is pasted whole, is for the right provider, and still has credit. |
| Jobs stay pending forever | Two causes. In a bring-your-own-key deployment this is expected when the tab was closed mid-generation — the poller has no key and cannot finish the job; reopen the studio and it resumes from /api/pending. Otherwise the poller is not running: check DISABLE_POLLER is not set on the only instance you have. |
| Duplicate generations, credit burning fast | Several replicas are polling. Set DISABLE_POLLER=1 on all but one. |
| Rate limits trip behind a proxy | The app trusts exactly one proxy hop. More than one reverse proxy makes every request look like it comes from the same address. |
| Assistant returns a key error | No Anthropic key. Set ANTHROPIC_API_KEY, or add one in Settings → API Keys. |
Licence & use
MIT. In plain terms: run it, modify it, self-host it, fork it, use it for client work, charge for that work, and keep the money. You do not owe us a licence fee, a revenue share, or a mention.
Attribution is appreciated and not required.
One thing to know before you deploy, because we would rather say it than have you find it. The build carries a provenance mark in three places: a .origin dotfile, an X-Origin-Sig response header sent on every request, and a keyword in package.json. All three decode to the same string naming Kanaky Tech as the author.
It phones nothing home and it gates nothing. You can delete all three — the header is a two-line middleware at the top of server.js, the rest are plain files. We put this in the documentation rather than leaving it to be discovered in a diff.
Custom builds
Generic software solves the generic eighty per cent. The remaining twenty — your naming rules, your catalogue schema, your approval chain — is where the hours actually go. Closing that gap is the work we sell, scoped with you rather than priced off a list.
3D models, music, sound design, editing, VFX. Features that don't exist yet, integration with the systems you already run, deployment on your own ground, your own models and endpoints, and a handover meant to make you independent of us. You own what we write, under the same MIT terms.