The marketing site for KeeForge, a KeePass-compatible iOS password manager, plus the source of the Cloudflare Worker that receives in-app feedback.
/— homepage (hero, screenshots, features, comparison, FAQ)/vs/keepassium— KeeForge vs KeePassium comparison/vs/strongbox— KeeForge vs Strongbox comparison/privacy— privacy policy/security-audit— public security audit record (English only)/appcast.xml— Sparkle update feed for the direct-download Mac app (see Sparkle appcast)
German, French, Spanish, Simplified Chinese, Traditional Chinese, Japanese, and Italian translations of every page except /security-audit live under /de/, /fr/, /es/, /zh-hans/, /zh-hant/, /ja/, and /it/.
npm install
npm run dev # http://localhost:4321
npm run build # outputs to dist/
npm run preview # serve the built sitesrc/components/HomePage.astro renders every localized homepage from
src/i18n/home.ts. Keep all eight locale objects aligned with the English copy.
DownloadLinks.astro owns the App Store platform links and direct Mac download;
ProductScreenshots.astro pairs real iPhone and Mac captures in the hero and each
feature section, with both vertically centered. The iPhone overlaps the left in the vault and editor
sections, and the right in the hero and organization section. iPhone images
must retain their intrinsic aspect ratio (height: auto), including at mobile
widths. src/styles/home.css scopes the cream, serif homepage design to .kf-home.
The existing shared language switcher is used in the desktop header, mobile menu,
and footer. FAQ items use native details controls.
Mac images live in public/screenshots/mac-entry-detail.png and
mac-entry-edit.jpg. These are real development captures with sample data.
Homepage copy distinguishes direct Dropbox/OneDrive integration on iOS from
synced local folders on Mac. WebDAV is available on both.
Run npm run build and node --test tests/home.test.js after
homepage changes. Check desktop and mobile layouts in all eight languages.
worker/index.js is the Cloudflare Worker behind
https://feedback.keeforge.com/api/feedback, the endpoint the KeeForge app
posts in-app feedback to.
The source is public on purpose. KeeForge's privacy policy tells users they can audit the code that handles their data, and this is the code that receives it.
npm run test:worker| Request | Response |
|---|---|
POST /api/feedback |
202 {"ok":true,"id":"<uuid>"} on success |
GET /api/feedback |
405 {"ok":false,"error":"method_not_allowed"} (on the feedback host, without an ASSETS binding) |
OPTIONS /api/feedback |
204, no body |
The app submits a narrow, sanitized payload:
{
"message": "string",
"details": "string",
"consentToContact": true,
"contact": "string (follow-up email)",
"photo": {
"data": "base64 string",
"contentType": "image/jpeg"
}
}consentToContact, contact, and photo are optional and omitted entirely
unless the user opts in. contact is only accepted, and only stored, when
consentToContact is true.
photo holds a single image the user attached. The app downscales it
on-device and re-encodes it as JPEG, stripping EXIF/GPS metadata, capped at
5 MB of image bytes.
details is empty for general feedback. For a database-open failure it contains
the error information and diagnostics that were displayed in the feedback form
before the user pressed Send: app and device metadata, cloud status, short
hash prefixes, encrypted file size, and a KDBX header summary.
The endpoint is intentionally narrow. It must never receive vault contents, entries, passwords, key files, raw database files, or unsanitized logs.
| Error | Status | Cause |
|---|---|---|
invalid_json |
400 | body is not valid JSON |
message_required |
400 | message is empty after trimming |
contact_required |
400 | consentToContact is true with no contact |
invalid_photo |
400 | malformed or empty base64 |
unsupported_photo_type |
400 | content type is not JPEG or PNG |
photo_too_large |
400 | decoded photo exceeds 5 MB |
payload_too_large |
413 | request body exceeds 8 MB |
rate_limited |
429 | blocked by the Cloudflare rate-limiting rule |
internal_error |
500 | unhandled failure |
Field lengths are capped server-side; see MAX_FIELD_LENGTHS in
worker/index.js. MAX_FIELD_LENGTHS also caps six top-level fields
(errorCode, errorCategory, appVersion, buildNumber, osVersion,
deviceModel) that older app builds sent; the current app folds that
information into details and never sends them, but the Worker still accepts
and stores them.
The Worker validates the payload, stores an attached photo in R2, and inserts one row per submission into D1. That is the entire request path — the feedback path makes no outbound requests and has no third-party integrations.
Submissions do not stop at D1, though. A separate maintainer-side tool polls the database and relays new submissions as a notification, so that feedback does not sit unread. That notification carries the message, the diagnostics, and the follow-up email address when one was given; it does not carry the attached photo.
See the privacy policy for what the feedback form collects and how long submissions are kept.
If the R2 binding is absent the submission is still stored, just without the attachment. Only a failed D1 insert fails the request.
The same Worker also handles browser-language routing for keeforge.com when
the private deployment config points site paths at it. On a GET it first
runs the locale logic, then falls through to normal static serving:
/redirects (302, uncached) to/<locale>/based on thekf_langcookie, or failing that theAccept-Languageheader; English stays on/.?setlang=1on any localized path records that choice in a one-yearkf_langcookie and redirects to the same path without the marker.- Every other path is a deep link and is never redirected.
/appcast.xmlis exempt from all of the above: no redirect and no cookie, even withAccept-Language,kf_lang, or?setlang=1.HEADis handled exactly likeGET.- Anything not redirected is served from the
ASSETSbinding when one is configured, and otherwise proxied to the static origin withfetch(request). Onfeedback.keeforge.comunmatchedGETs still return405.
public/appcast.xml is the Sparkle feed polled by the direct-download (non-App
Store) Mac app, which has https://keeforge.com/appcast.xml built in. It is
served with Content-Type: application/xml and a 5-minute cache via
public/_headers.
The feed starts with channel metadata and no items, and stays that way until a release is approved for production. Never add placeholder, candidate, or hand-edited items.
Update archives are hosted on immutable GitHub Releases in the app repo, never on this site:
https://github.com/KeeForge/KeeForge/releases/download/v{version}/KeeForge-{version}-b{repoBuild}.zip
Publishing an update (driven from the app repo's release tooling,
ci_scripts/release_direct_artifact.sh):
handoffbuilds the staged feed from the currentpublic/appcast.xmlplus the new item.sparkle:edSignatureandlengthcome from the exact signed ZIP; older items are kept, and duplicate versions or builds are refused.- After production approval, the GitHub Release is published and the asset is downloaded anonymously from the URL above. Its SHA-256 must match the staged artifact.
publish-appcast --destination public/appcast.xmlreplaces this file only if it is unchanged since staging. Then runnpm run test:worker, commit, and push.
Rules:
- Enclosures always use the versioned URL above, never
releases/latest/download. - Never commit ZIPs, signing keys, or any other release artifact to this repo.
- Never remove older items; Sparkle needs them for users on older builds.
The site is built with Astro and deployed by Cloudflare Pages (project
keeforge: npm run build, output dist/). Every push to main deploys
automatically, and that includes changes to public/appcast.xml. Requests to
keeforge.com/* pass through the Worker first, which handles locale routing
and otherwise proxies to the Pages origin.
The Worker's wrangler.toml, Cloudflare resource topology, rate-limiting
configuration, and operational runbook live in the private keeforge-infra
repo, which deploys this Worker from a sibling checkout of this repo. Contact
the maintainer if you need access.