Keou Studioby kanaky.xyz
MIT open source · every feature, nothing held back

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.

The Keou production engine: key bar, upload area and live activity log
The production engine. The key bar at the top is where your provider key goes — it stays in this browser.

Install

What you need

RequirementNotes
Node.js requiredVersion 20 or later.
PostgreSQL requiredAny recent version. Migrations run automatically at first boot — you do not create tables by hand.
Provider key requiredA KIE.AI or Fal.ai account. Each user pastes their own in the studio — the server does not need one.
Cloudflare R2 requiredFree 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

terminal
docker 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

  1. Create the project

    New project → Deploy from GitHub → this repository, or your fork of it.

  2. Add PostgreSQL

    Add a PostgreSQL service in the same project. Railway injects DATABASE_URL for you — do not set it by hand.

  3. Set the variables

    JWT_SECRET, and the four R2_* values if you are using storage.

  4. Deploy

    railway.json and the healthcheck are already in the repository. Enable auto-deploy on main to receive upstream updates automatically.

Your first visual

  1. Get a provider key

    Sign up at kie.ai — they give you free credits. Copy the long API key string.

  2. Paste it in the key bar

    Top of the studio. Press Save. It is written to this browser's localStorage and nowhere else.

  3. Drop a product photo

    Any reasonably lit shot works. A plain background helps but is not required — the scene gets rebuilt anyway.

  4. 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.

  5. 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.

Browser your provider key lives here only Express API routes · auth rate limits Engine keou-actions orchestration Provider KIE.AI · Fal.ai image · video · audio PostgreSQL users · jobs projects · history R2 storage uploads durable results Poller completes async jobs needs a server-side key X-Provider-Key your key, never stored store poll durable URLs, straight to the browser

What happens during a generation

You Keou Provider R2 photo + brief store the source generate — with your key task id your browser drives the polling — the key never leaves it status? (repeat) result URL (expires) copy to durable storage durable URL

Where things live in the repository

PathWhat it holds
index.jsEntry point. Pins the edition, then loads the server.
server.jsExpress setup: security headers, rate limits, route mounting, boot.
src/config.jsEvery environment variable, read in one place.
src/migrate.jsSchema migrations. Run automatically at boot.
src/poller.jsBackground completion of asynchronous provider jobs.
src/routes/One file per API surface.
src/lib/keou-actions.jsThe generation engine — validation, persistence, provider dispatch.
src/lib/studio-prompts.jsThe 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

VariableNotes
DATABASE_URLPostgreSQL connection string. Migrations run automatically at boot.
JWT_SECRETSession signing secret, 16 characters minimum. Generate with openssl rand -hex 32.

Storage — required to upload

VariableNotes
R2_ACCOUNT_IDCloudflare account identifier.
R2_ACCESS_KEY
R2_SECRET_KEY
R2 API token pair.
R2_BUCKETBucket name. Defaults to keou.
R2_PUBLIC_URLPublic 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

VariableDefaultNotes
PORT3401HTTP port.
EDITIONopensourceSet by index.js. Leave it alone.
DEFAULT_PROVIDERkiekie or fal.
KIE_API_KEY
FAL_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_KEYEnables the assistant. Without it the chat returns a clear error and nothing else is affected.
ANTHROPIC_MODELclaude-sonnet-5Model backing the assistant.
OPENAI_API_KEYSpeech-to-text for voice input in the assistant.
ADMIN_EMAIL
ADMIN_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_SSLSet 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_NAMEKeouShown throughout the interface.
AGENCY_IMAGE_QUOTA
AGENCY_VIDEO_QUOTA
999999Internal counters only. There is no billing in this edition — leave them high.
JWT_EXPIRES
REFRESH_EXPIRES
15m / 30dToken lifetimes.
DATABASE_SSL_STRICT
DATABASE_CA
Certificate pinning for managed PostgreSQL.
DISABLE_POLLERSet 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_EMAIL and ADMIN_PASSWORD for 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.

Railway: enable auto-deploy on 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

MechanismHow it works
Password storagebcrypt hashes. Plaintext is never stored.
SessionsShort-lived JWT access token plus a refresh token. Lifetimes are configurable.
API keysFormat 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 links128-bit random token, expiry capped at 90 days, optional access code.
Rate limitingPer-route. Auth is strict (15/min), generation is polling-tolerant, uploads support batch work.
HeadersHelmet, with a content security policy that includes your R2 domain automatically.
Outbound URLsUser-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 filteringRequests 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

MethodPathDoes
POST/api/generateImage from a source photo and a brief.
POST/api/videoVideo from a still.
POST/api/polishRetouch an existing render.
POST/api/remixRe-imagine in a new direction.
POST/api/adaptRecompose to other aspect ratios.
POST/api/upscaleUpscale a render.
GET/api/status/:type/:taskIdPoll a running job.
GET/api/packsAvailable pack definitions.
POST/api/packStart an export pack.
GET/api/pack/:packId/statusProgress of a running pack.
GET/api/pack/:packId/zipDownload the finished pack.
GET/api/pendingYour unfinished jobs — used to resume after a reload.
POST/api/report-failureReport a generation that came back wrong.

Content and organisation

RouteDoes
/api/uploadSource images and video.
/api/downloadSigned download of a result.
/api/historyYour library — list, tag, bulk-move, delete.
/api/projectsClients.
/api/campaignsCampaigns within a client.
/api/shareReview links and structured client feedback.
/api/toolsVoice-over, sound effects, image and video upscaling.

Account and team

RouteDoes
/api/authRegister, login, refresh, logout, change password.
/api/profileRead and update your profile.
/api/keysLong-lived API keys for scripts and MCP clients.
/api/teamInvite members, set roles, reset, remove.
/api/adminInstance settings, quotas, branding. Admin only.
/api/analyticsROI, clients, velocity, campaigns, savings. Admin only.
/api/activityInstance activity log. Admin only.
/api/dashboardDashboard 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.

MethodPathDoes
POST/api/jarvis/chatStreaming agent loop with the full toolset.
POST/api/jarvis/sttSpeech to text. Needs OPENAI_API_KEY.
POST/api/jarvis/ttsText to speech.
GET/api/conversationsChat history — list, create, rename, delete.

Troubleshooting

SymptomCause 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 failStorage is not configured. Set the four R2_* variables.
History shows a result, the image is goneThe 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 403The provider rejected the key. Check it is pasted whole, is for the right provider, and still has credit.
Jobs stay pending foreverTwo 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 fastSeveral replicas are polling. Set DISABLE_POLLER=1 on all but one.
Rate limits trip behind a proxyThe 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 errorNo 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.