Unreviewed draft

DRAFT FOR LEGAL REVIEW — This document has not been reviewed by a lawyer, is not in force, and creates no obligation on anyone. It must be reviewed before 4keys accepts any user. Do not publish it as a live policy.

Privacy policy (draft)

This draft describes what the 4keys service actually stores and where, as implemented in the code that runs it. It has not been reviewed by a lawyer and it is not in force.

It is written to be accurate rather than reassuring. Where the service does not support something a privacy policy would normally promise, this document says so instead of promising it.

1. Who is responsible for this data

The service is operated by [LEGAL ENTITY NAME], registered at [REGISTERED ADDRESS] under business registration number [BUSINESS REGISTRATION NUMBER], represented by [REPRESENTATIVE NAME].

Privacy questions go to [PRIVACY CONTACT EMAIL]. None of these details may be filled in by anyone other than the operator of the service.

[REVIEWER: confirm which data protection regimes apply to the operator and to the users it accepts, and what this section must state under each.]

2. What is collected, where it lives, and how long it stays

4keys stores its data in two places, both operated by Cloudflare: a D1 database and an R2 object store. Every private object key begins with the account identifier, and every database read and write is scoped to it.

What 4keys stores, where it lives, and how long it stays there.
DataWhere it is storedHow it gets thereRetention as implemented
Email address and display nameD1, accounts.email and accounts.display_nameTaken from the Cloudflare Access identity assertion the first time an authenticated request resolves the account.Kept for as long as the account row exists. Terminating an account sets its status to terminated and records the time; it does not delete the row, and there is no self-serve deletion. [REVIEWER: retention for terminated accounts is a decision the owner has not made.]
Pseudonymous account identifierD1, accounts.owner_id and accounts.access_subject_digest, plus the owner_id column of every other table and the first segment of every private R2 keyDerived by hashing the verified Cloudflare Access subject with SHA-256. The subject itself is never stored.For the life of the account.
Project content — titles, scene and block text, story bible entriesD1, projects.project_json and projects.titleWritten by the Studio autosave, which saves the whole project snapshot.Until the owner deletes the project. Deletion is a tombstone: the row is marked deleted and a daily sweep permanently removes the row, its asset rows, and its R2 objects 30 days later.
Uploaded media — images, posters, and videoR2, under owners/<account>/projects/<project>/assets/, with the file name, type, size, and alt text in D1 assetsUploaded by the owner from the Studio asset library, at most 25 MiB per file.Removed when the owner deletes the file, which releases the bytes and the stored count immediately. Deleting one file is refused while the project still references it, while an image generation is running on the project, or while an authoring proposal could name it. Anything the owner never deletes is removed by the same 30-day sweep that follows a project deletion.
Generated images awaiting reviewR2, under owners/<account>/projects/<project>/image-artifacts/, with the type, size, dimensions, and content hash in D1 image_artifactsProduced when the owner asks for an image and confirms the request. The prompt and the negative prompt are stored with the request, in agent_jobs.request_json and in the parameters Cloudflare keeps for the generation workflow. Only their length reaches an audit row or a log line.The image bytes are deleted immediately when the owner discards the image, when the owner accepts it into the asset library, and seven days after generation if the owner never reviews it. The metadata row stays as the record that the image existed and what became of it. The stored prompt is kept for as long as the job row, which is indefinitely. [REVIEWER: retention for the stored prompt is a decision the owner has not made.]
Revision snapshots of a projectR2, under owners/<account>/projects/<project>/revisions/Written before an authoring change is applied, so the change can be undone afterwards.Retained. They are deliberately excluded from the object collector, because deleting one would make an undo unrecoverable.
Shared-library content — library names, descriptions, world bible settings, uploaded source files, notes, and linksD1, library_collections, library_assets, library_collection_assets, and library_collection_projects; R2, under owners/<account>/library-assets/ for original bytes and owners/<account>/library-previews/ for derived document PDFsWritten when the owner creates or copies a reusable material or world library, uploads an original file, or adds a note, link, or project connection. A document preview is generated only when the owner asks to open a supported document; the browser receives the private PDF derivative, not the source file.There is no shared-library deletion route or time-based retention sweep today, so collection metadata, relations, original bytes, notes, and links remain until a deletion rule is implemented. Derived document PDFs are not a second permanent copy: each owner has a 256 MiB preview cache and the oldest derived PDFs are evicted when a new preview needs room. [REVIEWER: set a deletion and retention rule for shared-library originals and metadata before publishing a final privacy notice.]
AI direction text and the provider output produced from itD1, agent_jobs.request_json and agent_jobs.job_json; R2, under owners/<account>/projects/<project>/jobs/ for job artifacts and owners/<account>/projects/<project>/authoring/ for authoring source and compiled outputSubmitted by the owner with a direction job or an authoring proposal.Authoring source is deleted 7 days after a proposal reaches a terminal state, compiled output after 30 days, and unreferenced authoring objects after 30 days. Job rows and job artifacts have no retention rule at all today. [REVIEWER: disclose job rows and job artifacts as retained indefinitely, or set a rule before publication.]
Published story snapshotsR2, under publications/<slug>/versions/, with the slug, version, and status in D1 publicationsWritten by the owner's own publish action.Immutable once written. Revoking a publication changes its status in D1 so it stops being served; the stored object is not deleted by a revoke. Deleting the project that published it removes both the object and its D1 row in the 30-day sweep.
Operational audit records — which action was allowed or denied, and whenD1, audit_eventsWritten by the service on job, publication, and asset actions. The stored detail is limited to an allowlist of short identifiers and counts. A row for an action a service operator performed also carries audit_events.actor_id, a pseudonym for that operator, which is deliberately not shown to the account the action was taken on.No retention rule today. [REVIEWER: disclose audit records as retained indefinitely, or set a rule before publication.]
Approximate network addressNot stored, except as the one-way key described in the next row.The address Cloudflare reports for a public request is shortened and hashed in memory to key a rate limiter, and is not written to any table or object.Not applicable — nothing is kept.
Reports about a published storyD1, publication_reports and publication_suspensionsWritten when someone reports a published story, on a path that ships switched off — while it is off no report row is written at all. A report holds the story's address, one of seven fixed reasons, the day, and a one-way key derived from the reporter's shortened network address with a secret — it holds no message, no name, and no contact detail, because there is no field that could carry one. One report per reporter, per story, per day is kept.No retention rule today. [REVIEWER: disclose reports and suspensions as retained indefinitely, or set a rule before publication.]
Requests for access from people who do not have an account yetD1, access_requests — the address itself in access_requests.email, and a salted one-way key of it in access_requests.email_digestWritten when someone asks for access, on a path that ships switched off — while it is off no row is written at all. A request holds the address, one of four fixed reasons for asking, the time, and later the decision: whether it was approved or refused, which operator decided, and one of five fixed refusal codes. There is no message field and no name field, because there is no column that could carry one. One request per address is kept, and at most 200 undecided requests exist at a time; a duplicate address and a request that arrives at the cap are answered exactly like an accepted one and stored nowhere. The address is stored in plaintext, because the only thing an approval does is let an operator copy it into the Cloudflare Access policy by hand.How long the address is kept depends on what happened to the request. A refused request has its address erased in the same statement that records the refusal. A request nobody has decided within 30 days is deleted outright, address and all, and the person may ask again. An approved request whose person has not signed in within 90 days of the approval is deleted the same way, which also withdraws the approval — if they arrive later, they are refused and have to be approved again. A request whose person has since signed in has its address erased by a daily sweep 30 days after that first sign-in, once their address is in the account row instead — but only where the service is configured to check this queue at sign-in. Where it is not, and that is how the service ships today, the service never learns that the person signed in, so that row is instead bounded by the 90-day approval rule above. The one-way key and the decision record are kept for as long as the row is, so that a decision stays final and a refused address cannot silently queue again.
Request logsCloudflare's log sink, as structured JSON lines.Emitted by the service. Request and response bodies are never included, values under sensitive keys are replaced, and an error is recorded as a hash of its message rather than the message.Governed by Cloudflare's own settings, not by this service. [REVIEWER: state Cloudflare's retention setting once it is known.]

3. How an account is created

Access to 4keys is granted through Cloudflare Access. When an invited person makes their first authenticated request, the service hashes the verified subject of their Cloudflare Access identity and uses part of that hash as the account identifier. The original subject value is never stored.

The email address and display name that Cloudflare Access asserts are stored on the account row at the same moment. They are the only two pieces of personal contact data in the system.

Those two values are not shown on any public page, are not written into a published story, are not included in an audit record, and are replaced in log lines. The only place they are returned to a browser is the service health response for the signed-in account itself.

4. Why each of these is stored

The service stores the above for these purposes and no others:

  • Running the product: holding a project so it can be opened again, serving uploaded media into a preview, and serving a published story to its readers.
  • Identifying the account so that one person's projects, assets, and publications are never readable by another.
  • Executing a direction: sending the text an author wrote to the model that carries it out, and keeping the result so the author can review, apply, or undo it.
  • Operating the service safely: rate limits, quota enforcement, and an audit record of which actions were allowed or denied.
  • Diagnosing failures: structured logs that carry event names, identifiers, and counts, but never message text or content.

5. Who else processes this data

Four parties other than the operator are involved. Nothing is sold, and there is no advertising network, analytics vendor, or data broker in this list.

  • Cloudflare, Inc. — hosts the static site, runs the application, and stores the database, the objects, the job orchestration state, and the identity layer. All product data lives here.
  • The Moondo AI Broker — receives a sanitized selection context and a scratch workspace when an authoring request is made. It has no permission to change a 4keys project; compiling and applying a change stay with 4keys.
  • The AI provider behind the Broker — receives the text the Broker routes to it. Which provider that is depends on the Broker's own configuration. [REVIEWER: name the provider or providers before this document is published.]
  • A self-operated macOS machine — executes older direction jobs through a subscription to a coding assistant. The direction text for those jobs reaches that machine and the provider behind that subscription. The machine holds no other product state.

6. Backups

The database is exported nightly to offsite storage and those exports are kept for 35 days. Because the account row lives in the database, an export contains the stored email addresses and display names. Published story snapshots are copied nightly as well.

Uploaded media, revision snapshots, job artifacts, and authoring artifacts are not backed up. For those, deletion is final: once the 30-day sweep removes an object, no copy of it exists.

7. What you can and cannot delete today

This section describes the service as it is built, not as it ought to be. Read it as the honest limit of what can be done without an operator intervening by hand.

  • You can delete a project. Doing so marks it deleted immediately, hides it from the Studio, and revokes any story it had published. A daily sweep then permanently removes the project row, its asset records, and all of its objects 30 days later.
  • You can revoke a published story. It stops being served to new readers immediately; a copy already fetched by a reader or held in a cache may survive for up to five more minutes.
  • You cannot delete a single uploaded file. There is no route for it. The only way to remove uploaded media is to delete the project that holds it and wait for the sweep.
  • You cannot delete your account yourself. There is no self-serve deletion and no in-product route that erases the account row. An account can be disabled by the operator, which blocks further use, but the row and the stored email address remain.
  • You cannot delete an audit record or a stored job. Neither has a retention rule or a deletion route today.
  • There is no in-product export of your data. A copy of a project can be obtained only by asking the operator.
  • [REVIEWER: several rights a privacy policy usually states — erasure, portability, objection — have no implementation behind them. Decide whether they are promised and built, or disclosed as unavailable, before this document is published.]

8. Logs and error reports

Log lines are structured records with an event name, an account identifier, and counts. Values under keys that could carry content — anything resembling a prompt, a direction, a title, a message, a name, an email address, or a credential — are replaced before the line is written.

An error is recorded as its type plus a hash of its normalized message. The message text itself is never stored, never logged, and never returned in a response body.

9. Where the data is processed

Cloudflare processes the data on its global network. The self-operated machine that runs older direction jobs is located at [RUNNER HOST LOCATION]. The AI provider behind the Broker processes the text it receives wherever that provider operates.

[REVIEWER: confirm whether a cross-border transfer statement is required, and what it must say for each of the parties above.]

10. Age

Access is invitation-only and the service is not directed at children. [REVIEWER: set a minimum age and confirm what must be stated about it.]

11. Changes to this document

This draft carries no effective date because it is not in force. Once reviewed, it takes effect on [EFFECTIVE DATE] and changes will be announced through [CHANGE NOTIFICATION CHANNEL].