The short version
Jeani holds product photography that belongs to other people’s businesses, so the setup is deliberately boring. One hardened server. Object storage that serves nothing publicly. Uploads scanned before they are processed. Originals written once and never overwritten.
This page describes what actually runs, not what is on a roadmap. Where a control does not exist, it says so plainly rather than leaving you to infer it.
Everything here describes the production service at jeani.ai as at the date above. It is a factual description of how the service is built, not a warranty about outcomes, and it is not the contract: what we are obliged to do is in the terms and the data processing agreement, and what personal data we hold and why is in privacy. It is, though, the detail behind the security measures we are bound to maintain under that agreement, and we will not materially reduce them without telling account owners at least thirty days beforehand. When something here changes, the date and the version at the top change with it, and earlier versions are available on request.
| Control | How | Status |
|---|---|---|
| Encryption in transit | TLS on every public request, certificates renewed automatically, HSTS for a year. Inside the machine the API reaches Postgres and Redis over a private Docker network that is published to nothing. | In production |
| Malware screening | Uploads are streamed to ClamAV. A ZIP entry is screened inside the API before it is written to the bucket at all; a file uploaded through a signed URL lands in the bucket first and is screened by the analyse worker before it is decoded, processed or shown to anyone. An unreachable scanner fails the file rather than admitting it unscanned. Files above the scanner’s 50 MB ceiling are recorded as unscanned rather than blocked. | In production |
| Tenant isolation | Every lookup is filtered by your account. An id belonging to someone else comes back as not found. | In production |
| Audit log | Actor, action, target, IP and user agent on account-changing actions, and on bulk review actions and retries. Readable by owners and admins. A single approve, flag or restore is recorded on the asset instead. | In production |
| Backups | Nightly encrypted snapshot to a separate bucket under a separate key. Restore tested weekly. | In production |
| Multi-factor authentication | Not built. Signing in with Google inherits whatever your organisation enforces there. | Not yet |
| Independent penetration test | Not commissioned. | Not yet |
| SOC 2 or ISO 27001 | Not certified, and no audit under way. | Not held |
Where your images live
The application runs on a single dedicated server at OVHcloud in Sydney, on Ubuntu 24.04 LTS. Everything runs there as a container: the web app, the API, the image and QA workers, Postgres, Redis and the malware scanner. There is no second region and no failover, which is in what we do not claim yet.
Your image files are not on that server. They sit in Wasabi object storage in the ap-southeast-2 region, reached with credentials that belong to the application alone.
Nothing is served publicly
Nothing in the bucket is reachable without a signature. Jeani never produces a public URL for an object, and every read — a thumbnail in the review screen, an export download, a preview — goes through a URL signed for that one request, which we only sign after loading the asset, version or export through the ownership check that proves it is yours. Download and preview links are valid for fifteen minutes. Upload links are valid for an hour.
Object keys are tenant-prefixed — ws/<workspaceId>/proj/<projectId>/orig/… — and originals are keyed by asset id, so a storage key never contains one of your filenames.
Originals are immutable
An original is written once and never modified. Re-uploading the same relative path stores the new bytes under a new version rather than overwriting the old one, so an earlier original is never overwritten in place.
Every output is produced by decoding the image to pixels and re-encoding it, so the picture in an output is rebuilt rather than copied. Metadata is the exception, and the default here is the permissive one: a recipe keeps the colour profile unless you turn that off, and every built-in preset leaves it on. That setting keeps more than the ICC profile — it carries the original’s EXIF, XMP and IPTC too.
Whether any of it reaches the output depends on what else the recipe does. The default recipe, and every preset except Enhance Only, repositions the product on a fresh canvas, and that rebuild drops the original’s metadata before the encoder sees it. When a recipe changes nothing — no repositioning, no canvas change, no background work, no enhancement — the original is re-encoded directly and its metadata is copied across, so a payload hidden in EXIF can survive into that output. Turn the colour-profile setting off and metadata is stripped in every case. Your original always keeps everything it arrived with, since originals are never modified.
One caveat worth stating plainly: objects are written without an explicit server-side-encryption header, so encryption at rest for your images is whatever the storage provider applies by default. The database volume on the server is not separately encrypted by us either. The nightly backup is the exception — it is encrypted with its own key before it leaves the machine. We do not claim more than that.
Getting in
Passwords
A password is hashed with scrypt — a random 16-byte salt and a 64-byte derived key — and checked with a constant-time comparison. The password itself is never stored and cannot be recovered by us. The minimum length is 10 characters. There is no composition rule, and we do not yet check passwords against known-breached lists.
Reset links are random, stored hashed, single use and valid for thirty minutes. Consuming one sets the new password and revokes every session that user had, in the same transaction. The forgot-password endpoint always answers the same way, so it cannot be used to work out which email addresses have accounts.
Sign in with Google
Social sign-in is OpenID Connect: the authorisation-code flow with PKCE (S256). The state, code verifier and nonce are held server-side in a single-use record that expires after ten minutes. The ID token is verified against Google’s published keys with RS256 only, the audience pinned to our client id, with explicit issuer and nonce checks. An identity is linked to an account only when Google asserts the address is verified.
The callback carries a single-use handoff code, valid for two minutes and deleted the moment it is exchanged, so tokens never appear in a URL, a browser history entry or a referrer header.
Sessions
An access token is a signed JWT that lives fifteen minutes. A refresh token is 48 random bytes and only its SHA-256 hash is stored, so the token itself exists nowhere on our side. Refreshing rotates it: the presented token is revoked and a new one issued, which makes a refresh token single use. The session behind it lasts thirty days.
Signing out revokes that session — or every session the user has, when the call is authenticated and no specific token is presented. A completed password reset revokes all of them. An access token that was already issued stays usable until it expires, so a revoked session takes full effect within fifteen minutes at worst. Your browser keeps its session in local storage; Jeani does not use cookies to authenticate you. That is a trade-off, not a feature — see the note on Content-Security-Policy below.
Membership and workspace grants are re-read from the database on every request. Changing someone’s role or removing them takes effect on their next call, not when their token happens to expire.
A sign-in from an IP address and browser we have not seen for that user raises a security email. So does creating or revoking an API key, or changing a password. Security emails cannot be switched off.
API keys
An API key is shown once, at creation. We keep its SHA-256 hash and a 14-character display prefix so you can tell your keys apart, and nothing else. Keys carry any of four scopes — read, review, write, admin — and the highest one maps to a role for the duration of the request; a key created with no scope is treated as write. They can also carry an expiry. Every request re-checks the key against revocation, expiry and account status. An API key can never hold platform-admin rights.
Roles
Five roles, in rank order: viewer, reviewer, editor, admin, owner. Each of 22 named permissions has a minimum role — viewers read, reviewers review and restore, editors create projects and run jobs, admins manage workspaces, members, API keys and the audit log, owners manage the account.
A workspace grant can only narrow a member’s role, never widen it, and a member with at least one grant is confined to the granted workspaces for single lookups and list queries alike. That is what lets an agency give a client reviewer-level access to one workspace and nothing else.
Uploads are screened before they are processed
Every file that arrives within the scanner’s size ceiling is streamed to ClamAV before it is processed or shown to anyone — files inside an archive before they are written to the bucket at all — over the internal network to a scanner container that publishes no port.
Screening is fail-closed: an unreachable scanner fails the file rather than admitting it unscanned. What that means differs by path. Inside an archive, an entry that does not clear the scanner is never written to the bucket, so a scanner outage means an import brings in nothing. A file uploaded through a signed URL is accepted first — the object is in the bucket before we can scan it — and the analyse worker then deletes it and marks the asset failed rather than passing it on. So a scanner outage does not stop uploads being accepted; it fails them shortly after they land rather than before. And if the analyse worker is down or its queue is backed up, an unscanned object waits in the bucket until it is picked up, which is the honest version of this control.
A detection deletes the object, fails the asset, writes a security audit event and notifies the workspace admins. Objects above the scanner’s 50 MB ceiling are recorded as unscanned rather than blocked; the 50 MB per-file upload limit is what keeps that from mattering in practice.
The declared type is not evidence
We do not trust the content type a browser sends. When an upload completes, Jeani re-reads the head of the stored object, works out the real format from its magic bytes, and if the format is unsupported or disagrees with the file extension it deletes the object and marks the asset failed. Ten extensions are accepted, covering eight formats: JPEG, PNG, WebP, TIFF, BMP, GIF, AVIF and HEIC. Everything else is refused on declaration and skipped inside archives.
Every path you supply is normalised and checked before it is used. Backslashes are converted to forward slashes, leading slashes and ./ stripped, and any segment that is empty, ., .., or carries a control character or one of <>:"|?* is rejected outright. That is what stops a crafted filename writing into another project’s prefix.
Caps
- 50 MB per file, 50,000 files per upload session.
- Upload sessions expire after twenty-four hours.
- 8 MB for a JSON request body; 2 GB for a single archive upload, enforced by the API and again by the proxy in front of it.
Archives
A ZIP is posted to our server as one multipart body and written to a temporary file on disk while we read it; that file is deleted as soon as the import finishes. The archive is then read entry by entry with a streaming reader, and no entry is ever written to local disk — each one goes from the reader to the format check to the scanner and straight into the bucket, in memory. Each entry is capped at 50 MB, the archive at 50,000 entries and 10 GB uncompressed, and any entry that expands by more than 200:1 is skipped rather than extracted. The uncompressed total is counted from bytes actually streamed as well as from the header, because a header can lie. Encrypted entries, directories, macOS resource forks, dot-files, Thumbs.db and desktop.ini are all skipped.
Decoding
Untrusted image bytes are opened through a single wrapper that pins the decoder to 100 megapixels and reads sequentially, and the imaging library is held to two threads and a 64 MB operation cache for the whole process, which is what stops a decompression bomb exhausting the machine on those paths. The ceiling is not yet applied on every decode: a few later steps in the pipeline still decode without that wrapper and fall back to the library’s own, much higher default. Closing that gap is listed below with the rest of them.
Isolation and access
Jeani is multi-tenant. Every workspace, project, asset, job, export and recipe lookup is filtered by the caller’s account through the ownership chain. Asking for an id that belongs to another account returns not found rather than forbidden, so the API never confirms that someone else’s id exists. There is an integration test that holds this line: a second account gets a 404 on another account’s project.
Isolation is enforced in the application layer. Row-level security is not switched on in Postgres, so the access layer is the single enforcement point rather than one of two.
The audit log
Around forty kinds of event are written to an audit log — registrations, sign-ins, social sign-ins, API key creation and revocation, blocked uploads, retries and regenerations, bulk review actions — each with the actor, the account, the action, the target, the IP address and the user agent. Owners and admins can read it in the app or through the API, and export it as CSV. Audit writes are best-effort: a failed write never fails the request it was describing.
Single review decisions are the exception, which matters if you are trying to reconstruct who approved an image. Approving, flagging or restoring one asset is recorded against that asset — the reviewer, the action, any note and the time, returned by GET /v1/assets/:id — but it is not written to the audit log, so it is not in what an admin reads or exports, and no screen shows that history yet. The bulk versions of approve, restore and retry are audited, as are a single retry and regenerate. That gap is listed below with the rest of them.
One caveat that matters if you issue API keys. A key has no identity of its own in the log: it acts as the account’s longest-standing owner, and there is no column for the key, so an action taken with a key is written against that person and reads exactly like they did it themselves. The IP address recorded alongside it is the only hint.
Platform administration
A small set of endpoints is reserved for platform administration. They are gated by a flag on the user record that no part of the product can set — no route and no service writes it, so it can only be changed directly in the database by someone with server access. API keys never carry it. Those endpoints are not yet written to the audit log, which is listed below with the rest of the gaps.
Logs
Request logs redact the Authorization and Cookie headers and any password, secret, credential, API key or token field. Every request carries an id, echoed back on the response, and once a caller is authenticated the log line carries the account and user too, so an incident can be followed across the API and the workers.
Keeping it running
Backups
A timer runs a backup every night at 03:20 Sydney time: a compressed dump of the whole database, plus the environment file and the TLS certificate store. It is pushed offsite with restic to a different bucket, under a different credential from the one that holds your images, and the repository has its own encryption password on top.
Neither credential can reach the other’s bucket: the one the application holds has no access to the backup bucket, and the backup credential cannot read, write or delete one of your images. That boundary is between the two sets of credentials, not between the machine and the backups — both sets, and the password that encrypts the backup repository, live in the same environment file on the server. So the split contains a compromise of the application; it does not contain a compromise of the host, and we would rather say that than let the sentence do more work than it can.
Retention is 7 daily, 5 weekly and 12 monthly snapshots. Every run verifies that the dump decompresses and contains real schema before it leaves the machine, then has restic re-read 2% of the repository.
Every Sunday at 05:00 a second timer does the part most backup setups skip: it pulls the newest snapshot back out, restores it into a throwaway database beside the live one, and fails loudly if the newest dump is more than two days old or if the tables that matter did not come back. A backup that has never been restored is not a backup.
Your image bytes are not in that dump. They live in the object store.
TLS and headers
Caddy terminates TLS, obtains and renews certificates automatically, and redirects www to the apex. Every response carries HSTS for a year, including subdomains, X-Content-Type-Options: nosniff and a strict-origin-when-cross-origin referrer policy, and the server header is stripped. The metrics endpoint returns 404 from the internet. There is no Content-Security-Policy yet.
The server
The firewall denies all inbound traffic by default and allows only SSH, port 80 for certificate renewal, and 443. No database, cache, scanner, metrics or worker port is reachable from the internet.
SSH is key-only. Password and keyboard-interactive authentication are off, root login by password is refused, there is an explicit list of accounts allowed to connect, and four failed attempts in ten minutes earn an hour’s ban from fail2ban. SSH runs on a non-standard port as well as the default one. The distribution’s security updates install unattended. All four application containers run as a non-root user.
Rate limits
Every route except the two health checks is limited to 300 requests a minute — per API key when the caller presents one, and per IP address otherwise, which includes signed-in browser sessions. The authentication routes are limited to 10 a minute. A rejected request comes back as 429 with Retry-After and the usual X-RateLimit-* headers. The counter lives in the API process rather than in a shared store, so with more than one process the effective limit is per process.
Credentials you give us
Credentials for a connector you configure — an S3 or Wasabi bucket, SFTP, Google Drive, a Shopify, WooCommerce, Magento or Shopware store — are encrypted with AES-256-GCM under a 32-byte key before they reach the database, and masked wherever they are displayed. The key is mandatory in production. It lives in an environment file on the server, readable only by the account the application runs as (mode 600), not in a managed key service.
AI providers and your pixels
Every AI operation in the shipped production configuration is served by a provider that runs on our own server: deterministic local image processing, no network call and no vendor involved. It is a stand-in, not a model — every operation it records carries provider: mock, which the API returns on the asset detail endpoint (GET /v1/assets/:id) so you can check it with a read-scoped key. No screen in the app surfaces it yet. The orchestrator service that would talk to a model vendor sits behind an optional profile and is not started by a normal deploy, and the vendor keys in the production configuration are blank. Generative work that needs a model vendor would need one enabled first, and the note below says what we would do before that.
Two vendor adapters exist in the code and neither is switched on: Anthropic, for describing and comparing images, and Replicate, for upscaling, background removal and reference-conditioned generation. There is no adapter for any other model vendor, so there is no data path to one at all.
If we ever enable a model vendor for a paid operation, it will be named on this page and account owners will be told before it happens, not after.
What does leave the platform
- Email. Platform email is sent through the Gmail API from hello@jeani.ai using a token scoped to sending only, so the contents of a notification pass through Google. No image is attached to one.
- Sign in with Google. If you use it, this is an exchange with Google about your identity. No image is involved.
- Destinations you configure. A connector sends your finished images to the store, bucket or drive you pointed it at, because that is what you asked it to do.
Everything else stays between the server in Sydney and the object storage bucket in ap-southeast-2, the region Wasabi names Sydney. Two honest qualifications on that. The server and the bucket are both run by providers whose own staff can reach the infrastructure they operate, for support and maintenance, and we cannot see or control where those staff sit. And the location of a provider’s region is the provider’s statement, not ours — we rely on what OVHcloud and Wasabi publish, and privacy sets out how that works under Australian Privacy Principle 8.
What we do not claim yet
A security page that lists only strengths is not worth reading. Here is the other half.
- No certification. No SOC 2, no ISO 27001, no HIPAA or PCI-DSS. No audit is under way.
- No independent penetration test. Nobody outside the team has tested Jeani, and there is no bug bounty.
- No multi-factor authentication. This is the largest single gap and it is the next thing we are building. If your organisation enforces MFA on Google accounts, signing in with Google inherits it today.
- No lockout after failed sign-ins. Password guessing is slowed by the 10-a-minute limit on authentication routes and nothing else.
- Reusing a spent refresh token. It is rejected, but it does not revoke the rest of that user’s sessions.
- No Content-Security-Policy header. Worth reading together with the line above about session storage: your session, including the refresh token, is held in browser local storage rather than in an HttpOnly cookie, and there is no CSP to raise the cost of a script injection on jeani.ai. Neither is where we want it. Moving the refresh token into an HttpOnly cookie and shipping a CSP are the two items sitting behind multi-factor authentication on the list.
- Not every decode is capped. A few later steps in the pipeline decode without the 100-megapixel wrapper and fall back to the imaging library’s much higher default.
- No row-level security in the database. Tenant isolation is enforced in the application layer only.
- Platform-administration endpoints are not audit-logged.
- A single review decision is not in the audit log. Approving, flagging or restoring one asset is recorded on the asset with its reviewer and time, but only the bulk versions — and retries — reach the log an admin reads or exports.
- The audit log cannot identify an API key. A key acts as the account’s longest-standing owner, so its actions are recorded against that person and cannot be told apart from that person’s own.
- Metadata is kept by default. The recipe option that keeps the colour profile also keeps EXIF, XMP and IPTC, and it is on by default, so an output can carry the original’s metadata. Stripping by default, and stripping independently of the colour profile, are both on the list.
- No automated dependency or vulnerability scanning. Upgrades are done by hand.
- Encryption at rest is the storage provider’s default. We do not send an explicit encryption header and do not claim more than the provider does.
- No managed key service. Secrets live in a mode-600 environment file on the server, owned by the deploy account, and there is no automated rotation.
- No uptime or availability commitment. Jeani runs on one machine, with no failover and no second region.
- No published recovery-time or recovery-point target. In practice the recovery point is the previous night’s backup.
Who can reach production, and what does not follow from that
Production is reached by one person, over a single SSH key with password authentication disabled. There is no shared account, no vendor with standing access, and no console for anyone else.
That cuts both ways, so here is the other half of it. There is no periodic access review, no background-check process, no separation of duties, no documented change-management approval and no security-awareness programme — because there is no second person for any of those controls to operate on. Deployments are made by the same person from the same key. The audit log records what happened inside the product; it does not record what was done on the server. We have never rehearsed the incident procedure against a live event. If your procurement standard requires two-person control over production, or an independent reviewer of administrative access, Jeani does not meet it today and we would rather you learned that here than in a questionnaire.
What is yours to do
Some of the controls that matter most sit on your side of the line, and with no multi-factor authentication in the product today, more of them than we would like.
- Use a long, unique password, or sign in with Google so your organisation’s MFA policy does the work.
- Treat an API key as an account-wide credential — it is not confined to one workspace. Give it the narrowest scope that works, set an expiry, and revoke it the moment it is no longer needed or you think it has been seen.
- Remove people when they leave, and use workspace grants to confine anyone who should only ever see one client’s work.
- Read the security emails. A new-sign-in notice you were not expecting is the earliest warning you will get, and it is the reason those emails cannot be switched off.
- Check the audit log from time to time. Nobody is watching it on your behalf.
- Credentials you hand a connector reach your store or your bucket, not ours. Scope them there as tightly as that system allows.
These are the ones we cannot do for you, and we would rather set them out than imply the product covers them.
Nothing is deleted automatically today. The sweep that would delete originals after a retention period is built but nothing schedules it, and the retention period itself is an account-wide setting that can only be set through the API — there is no screen for it. There is no self-service account deletion either. Until that changes, assume what you upload stays until you ask us to remove it.
Ask from the address on the account. We will confirm it is you, then delete the objects and the database rows by hand and confirm in writing when it is done, within thirty days of a verified request. Two things survive that: records we are required to keep, and the encrypted nightly backups, which age out over about twelve months under the 7 daily / 5 weekly / 12 monthly schedule and are never restored except to recover the whole service. The data processing agreement is the binding version of this, and privacy sets out what we hold and why.
Reporting a vulnerability
If you find something, please tell us before you tell anyone else. Email hello@jeani.ai with security in the subject line. It reaches a person, not a queue. The same details are published at jeani.ai/.well-known/security.txt.
What helps
- What you found, and the URL or endpoint it affects.
- The steps to reproduce it, in order.
- What someone could actually do with it.
- A short proof of concept, which is worth more than a scanner report.
What we will do
We will acknowledge your report within two business days, tell you whether we can reproduce it, and keep you informed until it is closed. If you would like to be credited on this page, say so and we will credit you. There is no paid bounty.
We treat your report and your identity as confidential. We will not name you to anyone outside the people fixing it — including a customer we have to notify — without your agreement. If you would rather not send a working exploit to a shared mailbox, say so in a first message and we will arrange another channel.
What we ask
Section 6 of our terms asks you not to run penetration tests against Jeani without asking us first. This section is that permission — research that stays inside the lines below is authorised in advance, and you do not need to write to us before you start.
- Test against your own account and your own data. Please do not access, modify or download another customer’s images or records.
- No denial-of-service, load or brute-force testing against the production system. Those tests affect real customers mid-job and tell us nothing we want to know.
- No social engineering of our people, our customers, or our providers.
Safe harbour
If you stay inside the lines above, we will treat your research as authorised, and we mean that in the specific sense that matters to you.
- We authorise your access for the purposes of Part 10.7 of the Criminal Code Act 1995 (Cth) and any equivalent State or Territory computer-offence law, so that your access to Jeani is not unauthorised access.
- We will not bring a civil claim against you, and we will not refer you to law enforcement or to your employer, for the testing itself.
- We waive any claim under our terms of service and under the Copyright Act 1968 (Cth) arising out of that testing, and we will not treat it as a breach of your account agreement or grounds to suspend your account.
- If someone else brings a claim against you over research that stayed inside these lines, tell us and we will confirm in writing that the testing was authorised.
Three limits we cannot waive. We can authorise testing of jeani.ai and node.jeani.ai and nothing else — we cannot authorise you to test OVHcloud, Wasabi, Google or Cloudflare, and this safe harbour does not reach them. We cannot waive another customer’s rights or any other third party’s. And it does not cover going outside these lines deliberately: extracting another customer’s data, extortion, or selling a finding. Stepping outside them by accident and telling us promptly does not cost you the safe harbour.
If you are not sure whether something is inside the lines, ask us before you do it. We would rather answer the question than argue about it afterwards.
Publishing. We ask for ninety days from your first report before you publish. If we fix it sooner we will tell you, and you are free to publish from that point. If we need longer we will explain why and agree a date with you rather than set one ourselves. We will not make indefinite silence a condition of anything in this section.
If you come across someone else’s data. Stop. Keep no more of it than you need to show us the problem, tell us what you saw, and delete your copy once we confirm we have what we need — then tell us you have. We may have to treat what you saw as a data breach and notify the customer affected, so an accurate account from you is what makes that notification honest.
Status and incidents
The status page shows whether the API, the database and the job queue are answering right now. It reports those three, plus the fact that the site itself served the page, and nothing else — that is all we can honestly measure. There is no uptime percentage on it, because we have no measurement history to compute one from and would rather show you nothing than show you a number we made up.
If something is broken for you and the status page says everything is fine, that is worth knowing — tell us at hello@jeani.ai.
If customer data is exposed
If we become aware of a security incident affecting customer data, we will tell the owners and admins of every account affected without undue delay, and in any case within 48 hours of becoming aware of it — not within 48 hours of finishing the investigation. That is the commitment in the data processing agreement, and it is deliberately shorter than the 72 hours a controller has to notify a regulator, so you have time to make your own report. The first message carries what we know at that point, which categories of data are involved, what we have done to contain it, and what you should do at your end; we follow up as the picture fills in rather than waiting for it to be complete. A written account follows within five business days of the incident closing. Sessions and API keys can be revoked immediately, which is the first thing we would do.
Where the Notifiable Data Breaches scheme applies and the breach is likely to result in serious harm, we also notify the Office of the Australian Information Commissioner and the individuals affected, as the Privacy Act requires. Where we hold your catalogue as your processor, notifying your own regulator is your decision as controller, and the data processing agreement sets out what we owe you to make it.
There is no published incident history here yet.