# Postcard: a Rails site and newsletter you host yourself instead of a social feed

> Postcard is a GPL-3.0 Rails application that gives one person a personal website, an integrated newsletter and light blogging, with Open Graph images generated at request time. It ships in solo mode for self-hosting and in multiuser mode behind postcard.page, and the payment stack deliberately sits on an older version of the Pay gem.

**contraptionco/postcard** — Personal website + newsletter as an alternative to social media.

- Repository: https://github.com/contraptionco/postcard
- Website: https://postcard.page
- Stars: 601 · Forks: 45
- Language: Ruby
- License: GPL-3.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/contraptionco-postcard

## A personal site, a newsletter and a blog in one Rails app

Postcard is a Ruby on Rails application for people who want to publish without an algorithm in the middle. It combines a personal website, an integrated newsletter and light blogging in one codebase, and the pitch is that you write once on your Postcard and share the same post everywhere. The project states that posts automatically include meta tags and Open Graph images that look professional when shared on social platforms, so the point is not to avoid social platforms but to stop depending on them as the home for the content.

Two mechanisms are worth isolating. The first is the newsletter, which is the mailing list side of the product: subscribers are collected on your own domain and the mail is sent through your own configuration rather than through a third party list manager. The second is Open Graph image generation, which the requirements list names as the reason Node.js and npm are needed, because Puppeteer renders a browser screenshot for the share preview.

That second mechanism has a consequence worth stating early, because it is the least obvious dependency in the stack. Rendering images means running a browser inside your application, which means shipping Chromium, keeping it patched, and accepting the memory cost of a headless browser next to the Rails process. The repository holds the Puppeteer version in package.json and treats the install script as something to review on every upgrade.

## Solo mode by default, multiuser mode behind postcard.page

The repository runs in solo mode, which the documentation recommends for open source users. Solo mode is intended for running one website, without the multi-domain setup or paywalls that the hosted service deals with. The instructions are blunt about it: fork the repository and run it on your own server with Docker and a Postgres database.

The same codebase runs in multiuser mode, and that is what powers postcard.page, where you can try the service or pay to have it hosted on your own domain. The distinction matters when you read the documentation, because configuration and environment variables differ. In multiuser mode, the `BASE_HOST` variable's own description says subdomains become public account pages, which is a concept that simply does not apply to a single site.

The application code also branches on the mode in the test suite, where the same tests run twice, once with `APP_MODE=SOLO` and once with `APP_MODE=MULTIUSER`, and CI runs them separately against PostgreSQL 16. A fork that intends to stay single site should keep that mode distinction in mind, because behaviour that works in solo mode may assume a single account in ways that surprise you later.

## Running it locally needs Ruby, Postgres, libvips, Node and a browser

The dependency list is long but explicit. Ruby 3.4.10 and Rails 8.1.3.1 or a newer 8.1 patch release are required, PostgreSQL 16 is recommended, libvips 8.13 or newer is needed for image processing, and Node.js 24 LTS with npm is used for Open Graph image generation with Puppeteer. The exact Node version lives in `.node-version`.

The development quick start is five steps: install the Ruby gems, install the locked JavaScript dependencies and the matching browser, set up the database, start the Rails server, and open the application. The server is started with `./bin/dev-solo`, and the application is then reachable on port 3000 at `localtest.me` rather than `localhost`.

That odd hostname is not a mistake. The documentation explains that straight localhost cannot be used because of hCaptcha constraints in local development, so the project routes development traffic through a domain that resolves to the local machine. Anyone who tries to run this and sees a captcha error on `localhost:3000` has hit the same wall, and the fix is already in the quick start.

The full local check set is the best summary of what the project expects to be correct:

```sh
RAILS_ENV=test bundle exec rails db:prepare
bundle exec rails zeitwerk:check
APP_MODE=SOLO COVERAGE=1 bin/test
APP_MODE=MULTIUSER COVERAGE=1 bin/test
npm test
bundle exec brakeman
bundle exec bundler-audit check --update
```

Two Rails test databases are involved, one dedicated to local testing, and the documentation warns never to use a production `DATABASE_URL` for these commands because the Rails tests reset the database contents. The last two commands are the static analysis steps, brakeman for the Rails code and bundler-audit for the gems.

## Two services decide whether the newsletter works at all

In development, Postcard works with minimal configuration and a `.env` file is optional. In production two external services are needed. AWS handles email sending, and hCaptcha handles spam prevention, which the project notes is free.

The AWS variables are the ones that affect uploads rather than mail. `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are required in production for storing user uploads such as profile photos and attachments, and `AWS_REGION` points at the bucket location, defaulting to `us-east-1`. The rest of the identity settings are cosmetic but user visible: `DEFAULT_EMAIL_FROM` defaults to `hello@postcard.page` and `DEFAULT_EMAIL_FROM_NAME` defaults to `Postcard`, and those are the sender name and address your subscribers see.

Two more variables are worth reading twice. `BASE_HOST` is the domain for the installation, defaulting to `localtest.me` in development and `postcard.page` in production. `SECRET_KEY_BASE` defaults to null and is used by Rails for session cookies and other cryptographic operations, and the documentation is explicit that it should be a long random string kept secret. It is the one variable in the table whose default is not a usable value.

For everything else, the project points to `config/initializers/app_config.rb` as the complete reference for available configuration options. That is the file to read before deploying, rather than the table above, which the documentation presents as the solo mode list.

## The payment stack stays on Pay 5 and backports a security fix

Postcard uses the Pay gem for subscriptions, and the version choice is a deliberate constraint rather than an oversight. The payment dependency stays on version 5 to keep existing Stripe subscription records compatible, because the newer major versions require explicit schema and data migrations, and those migrations have to be tested as a separate billing upgrade rather than folded into an ordinary release.

The application carries a backport of the payment return-link fix for CVE-2023-30614, restricts payment return links to local paths, and enables only the Stripe processor. That last point defines the scope of the feature set: Braintree and Paddle are not enabled paths in this application.

The audit configuration follows from the same constraint. Two exceptions are documented in `.bundler-audit.yml`: the tested backport, and the Paddle Billing signature advisory, whose controller was introduced after Pay 5 and is therefore absent from this codebase. The documentation says plainly that these exceptions do not mean the payment dependency is current, and asks that they be reviewed when Pay is upgraded. Mounted-route tests cover the return link and the unavailable Paddle and Braintree webhooks, so the inert code paths are at least exercised by the suite.

For an operator, the practical read is that subscription billing works through Stripe, that the code carries a known advisory with a local fix, and that moving to a current Pay version is a project with its own testing budget.

## Containers, health checks and files that must agree

The Dockerfile shows how the deployment is assembled, and its first job is to keep the runtime versions pinned in step with the rest of the repository:

```dockerfile
ARG RUBY_VERSION=3.4.10
ARG NODE_VERSION=24.20.0
FROM --platform=linux/amd64 node:${NODE_VERSION}-bookworm-slim AS node
FROM --platform=linux/amd64 ruby:${RUBY_VERSION}-slim-bookworm AS base

WORKDIR /rails
```

Node and Ruby are pulled into separate stages and the Node binary is copied across, which the comment explains avoids compiling Node on every image build. The production environment is set in the same file, with static file serving, logging to standard output and deployment mode bundling:

```dockerfile
ENV RAILS_ENV="production" \
    BUNDLE_WITHOUT="development:test:preview" \
    BUNDLE_DEPLOYMENT="1" \
    RAILS_SERVE_STATIC_FILES="true" \
    RAILS_LOG_TO_STDOUT="1" \
    GROVER_NO_SANDBOX="true" \
    PUPPETEER_CHROME_HEADLESS_SHELL_SKIP_DOWNLOAD="true" \
    PUPPETEER_CACHE_DIR="/rails/.cache/puppeteer"
```

Two of those settings exist because of the image rendering. The Chromium download is skipped during the build because the locked Puppeteer package handles it, and the sandbox is disabled for Grover, the other screenshot path the CI uses.

The CI job mirrors the local checks and adds a container check that runs the default production entrypoint against a disposable PostgreSQL with dummy service credentials, requests the health endpoint and verifies that Puma wrote its PID file. The health endpoint is therefore the thing to point a load balancer at. The repository also contains a `render.yaml` and a `render-proxy/` directory, so a Render deployment is an anticipated path.

One consistency rule is stated more than once because breaking it is easy: the Ruby versions in `.ruby-version`, `.tool-versions`, `Gemfile` and `Dockerfile` must stay aligned, and Node uses `.node-version`, `.tool-versions` and the Dockerfile.

## Licence, releases and the shape of the alternative

Postcard is released under the GPL open source licence and is a project of Contraption Company. The manifest in package.json, which is the JavaScript side of the project rather than the Rails application, records `ISC` as its own licence, so the two files describe different scopes. The GPL applies to the application you are forking and running, and that is the term to plan around when you change the code and host the result.

The repository publishes no GitHub releases, so there is no tagged version to point an upgrade at. The last push was on 2026-10-12, and the package manifest reads version `1.0.0`. Upgrading a self-hosted Postcard therefore means tracking the default branch rather than pulling a version tag, which makes the Rails and Node version alignment rule the practical guide to when a pull is safe.

The alternative worth weighing is the hosted multiuser service rather than another piece of software. Running Postcard yourself means owning the database, the mail sending, the captcha provider, the headless browser and every Rails upgrade, and you gain a single site with no paywall logic and no account pages. Using postcard.page means someone else runs that infrastructure and you give up the fork-and-change model that the open source route exists to offer. A third path, a static site generator for the writing and a separate mailing list service for the subscribers, gives a lighter stack with none of the Open Graph rendering or the Rails upgrade cycle, at the cost of not having posts and subscribers in one system.

Between those, the deciding question is not features but who carries the upgrades.

## Conclusion

Postcard suits a writer who wants a personal domain, a mailing list and posts that share with proper Open Graph previews, and who is prepared to run Rails, Postgres, Node and a headless browser on their own server. It does not suit someone who wants a hosted solution with no infrastructure, since the hosted option is a separate multiuser service rather than a download, and it does not suit a project that needs Paddle or Braintree payments, because only the Stripe processor is enabled. Before deploying, read config/initializers/app_config.rb for the full option list, keep the Ruby version aligned across .ruby-version, .tool-versions, Gemfile and Dockerfile, and review the two .bundler-audit.yml exceptions, which the project says do not mean the payment dependency is current.

## FAQ

### What does Postcard need installed to run locally?

Ruby 3.4.10, Rails 8.1.3.1 or a newer 8.1 patch, PostgreSQL 16, libvips 8.13 or newer, and Node.js 24 LTS with npm for Puppeteer Open Graph rendering. The application is then served with `./bin/dev-solo` and opened at `localtest.me:3000`.

### Why does Postcard use localtest.me instead of localhost in development?

The documentation explains that straight localhost cannot be used because of hCaptcha constraints during local development, so the project points local traffic at a domain that resolves to the local machine.

### Which payment processors does Postcard support?

Only the Stripe processor is enabled. The application stays on version 5 of the Pay gem to keep existing Stripe subscription records compatible, and backports the return-link fix for CVE-2023-30614 while restricting payment return links to local paths.

### What environment variables must I set in production?

At minimum `SECRET_KEY_BASE` for session cookies and cryptographic operations, the AWS credentials and `AWS_REGION` for storing uploads, and a `BASE_HOST` for your domain. AWS is also used for sending mail, and hCaptcha is used for spam prevention.

### Does Postcard publish release versions I can install?

The repository has no GitHub releases, so there is no tagged version to pin. The package manifest reads version `1.0.0` and the last push was on 2026-10-12, so upgrades are tracked from the default branch, which is why the Ruby and Node versions have to stay aligned across `.ruby-version`, `.tool-versions`, `Gemfile`, `.node-version` and the `Dockerfile`.

## Sources

- [contraptionco/postcard on GitHub](https://github.com/contraptionco/postcard)
- [Issues](https://github.com/contraptionco/postcard/issues)
- [License: GPL-3.0](https://github.com/contraptionco/postcard/blob/main/LICENSE)
- [Project website](https://postcard.page)
- [README](https://github.com/contraptionco/postcard/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/contraptionco-postcard
