Deployments for https://forwardapphq.com

Peter Theill
Peter Theill 
deployed a CI/build change 16 days ago
16 days ago
💚 Build the production image with Ruby 3.4.9
config/deploy.yml passed RUBY_VERSION to the Docker build and still said
the old Ruby, overriding the Dockerfile and failing bundle install.
Peter Theill
Peter Theill 
deployed a CI/build change 16 days ago
Peter Theill
Peter Theill 
deployed a documentation update 17 days ago
Peter Theill
Peter Theill 
deployed a security update about 1 month ago
about 1 month ago
🔒️ Blocks hold on the website too; reports only reach what the reporter can see

  • The web comment form and "ask for this item" now honour blocks, and the


rule lives in the model (Donation#open_to?, a Comment validation), so any
future path is covered as well.

  • The web comment controller hides drafts like the donation page does.

  • Reports of a draft listing, or of a comment on one, 404 for anyone but


the owner, so they can't be used to find out that a draft exists.
Peter Theill
Peter Theill 
deployed a documentation update about 1 month ago
about 1 month ago
:shield: Report, block, filter and terms for user content; new privacy policy and terms; OpenStreetMap tiles
What App Review guideline 1.2 asks of an app with user content:

  • Reports of listings, comments and people (POST /api/reports), emailed to


support straight away.

  • Blocks (/api/blocks): a blocked person's listings, comments and activity


are hidden, their requests are withdrawn, and they can no longer ask for
or comment on the blocker's things.

  • A whole-word filter (config/content_filter.yml) on listing titles,


descriptions and comments, in all seven languages.

  • App users must accept the terms (POST /api/me/accept_terms) before they


post, comment or ask for things.
The privacy policy now names every service that handles data (Hetzner,
Resend, OpenAI for photo suggestions, the sign-in providers), how long data
is kept and how to delete an account; the terms state zero tolerance for
objectionable content and how reports are handled. Both are now in all
seven languages instead of English only.
Maps use OpenStreetMap's tiles, with the attribution they require, instead
of Carto.
Peter Theill
Peter Theill 
deployed a feature about 1 month ago
about 1 month ago
🌐 Answer the API in the asker's language, photo suggestions too
The API skipped localized routes and so always spoke the default language:
errors, activity messages and notification text reached a Danish iPhone in
English. It now follows Accept-Language. Photo suggestions (title and
description) are written in the request's language as well, on the web
form too.
🔒️ Scope API tokens, use an https sign in callback, never link Apple sign in by email

  • ApiTokens get a scope: mcp tokens (made on the settings page) only work on


/mcp, and the iPhone app's tokens only on /api.

  • The website hand-off redirects to https://<host>/app/callback, which iOS


only gives to the app named in the new apple-app-site-association, instead
of a custom URL scheme any app could claim.

  • Sign in with Apple no longer signs in to an existing account with the same


email, since emails here are not verified.
✨ JSON API for the new iPhone app
Bearer token auth on /api with the existing ApiTokens, Sign in with Apple
(POST /api/session/apple), and a website hand-off at /app/authorize so every
existing way of signing in works in the app without it handling passwords.
Adds posting with a photo, editing and deleting, requesting, approving and
rejecting, regions, charities filtered by currency, notifications, and the
account (including deleting it, which App Review requires).
Peter Theill
Peter Theill 
deployed a bug fix about 1 month ago
about 1 month ago
🐛 Return real image URLs from the JSON API and stop leaking internal fields
The API serialised donations with as_json, so image_url came back as the
Active Storage attachment (with the whole record nested inside) instead of
a URL, and every column was exposed - requesters, unsubscribers, transfer,
transaction_id, and a comment's payment details.
Responses are now built field by field, with image_url, thumbnail_url,
charity logo_url and user avatar_url as URLs. Drafts 404 through the API
for anyone but their owner, matching the web page.
✨ Double down on SEO and AEO
Structured data on every public page (Organization sitewide, Product,
NGO, Place, ProfilePage, CollectionPage, Article, BreadcrumbList),
robots directives with noindex on personal and search-result pages,
a sitewide RSS feed, image entries in the sitemap, and llms.txt /
llms-full.txt rendered from the database instead of static files.
Translate the SEO category landing pages and giving guides into
Swedish, Norwegian, German, French and Spanish, and move the guides'
hardcoded badges and calls to action into locale keys.
Peter Theill
Peter Theill 
deployed a bug fix 2 months ago
2 months ago
🐛 Use the app icon for all favicon variants
favicon.ico, icon.svg and apple-touch-icon.png were leftovers from project creation and did not match icon.png. Regenerate the ICO (16/32/48px) and the 180px touch icon from icon.png, and rebuild icon.svg as a vector version of the same icon.
Peter Theill
Peter Theill 
deployed a styling change 3 months ago
Peter Theill
Peter Theill 
deployed a feature 3 months ago
3 months ago
✨ MCP endpoint for importing donations from Facebook groups (#24)
Adds an MCP server at POST /mcp so an AI agent can turn posts from a public
Facebook group into donations on Forward.
The server never touches Facebook. The agent does the reading, with its own
browser session and its own membership of the groups; the endpoint only turns
"here is a post" into a record. That keeps scraping out of the application and
leaves the MCP surface small enough to stay stable.
Three properties are enforced structurally rather than by convention:

  • Imports are drafts. A new `draft` state is invisible everywhere, because


every listing already goes through `active`, and the direct URL 404s for
anybody but its owner. Watch alerts are held back until `publish!`.

  • Imports are credited to the original poster. `user_id` stays the importing


account - a row needs an owner, and that is who to ask about it - while the
donation is attributed to `source_author_name` and links to `source_url`.
Imports are not requestable, because the importer does not have the item;
visitors are sent to the original post instead. `create_donation` requires
both fields, so there is no way to use this endpoint to post something as
your own.

  • Re-scanning a group is safe. `source_url` is unique at the database, and is


normalised to drop fbclid, mibextid and friends first, so the second import
of a post is refused rather than duplicated.
Two changes fall out of the ownership split. The donor-currency check is
skipped for imports, since `user` is the importer and their region says nothing
about the market the donation belongs to - somebody in Copenhagen curating a
Stockholm group is importing SEK donations, and that is correct. To keep the
invariant that check was protecting, `User#update_stats!` now counts only
donations the user posted themselves, so a curator is neither credited for
items they never had nor able to mix currencies into their totals. The region
and charity checks are untouched.
`list_charities` takes a region and returns only charities whose currency
matches it. The currency rule is the constraint an agent would otherwise trip
over on most calls, and answering it in the tool signature beats answering it
in an error message.
Image URLs arrive from a page an agent was told to go and read, so they are
attacker-influenced in the ordinary case. `RemoteImageFetcher` is https-only,
resolves the host and checks every address it resolves to, pins the connection
to the address it checked, and re-runs all of that per redirect hop.
Authentication is a bearer token rather than the session cookie, because the
caller is not a browser. Tokens are stored as HMAC digests, shown once, can be
named and renamed, and are rate limited per token.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
⬆️ Upgrade libraries
💸 Scope donation currency to the market (#23)
Currency becomes a property of the region a donation happens in rather than
something the donor picks per item.

  • a region owns the currency; a charity accepts exactly one, which keeps its


total raised a single figure

  • a donation copies its region's currency and validates it against the region,


the charity and the donor's own region

  • the locale decides only how an amount is punctuated, never which currency

  • the currency picker is gone from the donation form; `market_controller` keeps


the symbol and charity list in step with the selected region

  • `currency_symbol` columns are dropped for a new `Currency` model, following


`Locale` as the single source of truth

  • the admin dashboard's cross-market total is grouped by currency, not summed


Also makes `regions.donations_count` a true counter cache. An active-only
counter cannot work, since claiming or deleting a donation is an update and
never fires the create/destroy callbacks that maintain it. The regions index,
which labelled that number "available items", now queries for the active count.