Before you start
Campsend is one Rails application backed by SQLite. It runs as a single container with the databases on a volume, so there's no database server to operate.
You'll need:
- A server with Docker. One host. Campsend isn't built to run more than one writable copy at a time.
- An SMTP service. Campsend emails sign-in links and delivery links. Without working email nobody can sign in.
- Somewhere to keep files. A persistent volume on the server, or a private S3-compatible bucket.
- A hostname with TLS. Kamal gets the certificate for you.
To work on Campsend locally you'll need Ruby 3.3.4 and SQLite 3 instead.
Running it locally
Start here before you deploy. The local setup uses SQLite and writes email to a file, so you can see the whole product working without any credentials.
git clone https://github.com/obakeng-develops/campsend.git
cd campsend
bin/setup
bin/dev
bin/setup installs the gems and prepares the SQLite databases. bin/dev starts the server on localhost:3000.
Then send your first delivery:
-
Signing in
Enter any email address at
/session/new. Campsend writes the email totmp/mailsinstead of sending it. Open that file and follow the link inside. -
Sending a file
Pick a file, enter a recipient address and a note, then send. The delivery email lands in
tmp/mailstoo. -
Checking what was recorded
Follow the delivery link and download the file. Your Sent page now shows when the delivery was opened and when it was downloaded.
🚨 NB: sign-in links are single use and last 15 minutes. If you open one twice, the second attempt tells you it expired.
Choosing where files live
Campsend writes uploaded files to a volume on the server or to a private S3-compatible bucket. Pick one before you deploy. Changing ACTIVE_STORAGE_SERVICE later doesn't move anything, so files stored under the old service stop resolving.
Files sit on the persistent volume at /rails/storage, next to the databases. There's nothing else to configure and the backups you already take cover them.
ACTIVE_STORAGE_SERVICE=local
Keeps files off the application volume, so you can rebuild the server without moving data.
ACTIVE_STORAGE_SERVICE=s3
STORAGE_ENDPOINT=https://s3.us-east-1.amazonaws.com
STORAGE_REGION=us-east-1
STORAGE_BUCKET=campsend
STORAGE_ACCESS_KEY_ID=...
STORAGE_SECRET_ACCESS_KEY=...
STORAGE_FORCE_PATH_STYLE=false
R2 uses the region auto and path-style requests. If presigned upload URLs come from a different origin than STORAGE_ENDPOINT, also set STORAGE_BROWSER_ORIGIN to that origin.
ACTIVE_STORAGE_SERVICE=s3
STORAGE_ENDPOINT=https://ACCOUNT_ID.r2.cloudflarestorage.com
STORAGE_REGION=auto
STORAGE_BUCKET=campsend
STORAGE_ACCESS_KEY_ID=...
STORAGE_SECRET_ACCESS_KEY=...
STORAGE_FORCE_PATH_STYLE=true
Use the S3-compatible endpoint for your bucket's region. The native B2 API endpoint won't work.
ACTIVE_STORAGE_SERVICE=s3
STORAGE_ENDPOINT=https://s3.us-west-004.backblazeb2.com
STORAGE_REGION=us-west-004
STORAGE_BUCKET=campsend
STORAGE_ACCESS_KEY_ID=...
STORAGE_SECRET_ACCESS_KEY=...
STORAGE_FORCE_PATH_STYLE=true
Securing the bucket
Keep the bucket private. Give the credentials read, write and delete on that one bucket and nothing else. Campsend never needs account administration.
Browsers upload straight to the bucket, so it needs a CORS rule. Replace the origin with your own:
[
{
"AllowedOrigins": ["https://campsend.example.com"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["Content-Type", "Content-MD5", "Content-Disposition"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
Configuring email
Campsend sends sign-in links and delivery notifications over SMTP. Point it at any SMTP service.
APP_HOST=campsend.example.com
[email protected]
SMTP_ADDRESS=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=...
SMTP_PASSWORD=...
APP_HOST is the hostname on its own, without https://. Campsend uses it to build links and to authorize incoming requests, so a wrong value locks you out of your own installation.
🚨 NB: set up SPF and DKIM on the sending domain. If sign-in email goes to spam, nobody can sign in.
Deploying with Kamal
Campsend ships with a Kamal configuration. Kamal builds the image, pushes it to your registry and runs it on your server behind a proxy that terminates TLS.
-
Filling in the configuration
Edit
config/deploy.ymlwith your server address, image registry and hostname. Put every secret in.kamal/secrets, which is gitignored. -
Generating a master key
Each installation needs its own
RAILS_MASTER_KEY. Don't commit it, and don't reuse one from another deployment. -
Running setup once
Run
bin/kamal setup. It provisions the server, mounts the volume and starts Campsend. Every release after that needsbin/kamal deploy.
bin/kamal setup # first time
bin/kamal deploy # every release after that
The container entrypoint migrates the database before Puma starts, so you don't run migrations yourself.
Verifying the deployment
/upreturns HTTP 200.- A sign-in email arrives and the link works once.
- A small file uploads and appears in My Files.
- The recipient signs in and sees the delivery under Shared with me.
- Downloading the file records the event on your Sent page.
- Your backups include the persistent volume.
Keeping it running
Campsend keeps SQLite on a volume mounted at /rails/storage. That volume holds the application, cache, queue and cable databases. Run one writable Campsend host against it.
Keep Solid Queue running or scheduled deliveries never publish. The default deployment runs it inside Puma with SOLID_QUEUE_IN_PUMA=true. Split it into its own job process before you add a second web host.
Back the databases up with a SQLite-aware snapshot, not a file copy of a database that's being written to. Test a restore. Watch free disk space.
Campsend writes one JSON event per request and per job to standard output. Each line carries the request ID, route, status, duration and the records involved. Email addresses, access tokens and file names are left out. Ship those lines wherever you keep logs and you can answer most questions from a single row.
Configuration reference
Campsend reads production configuration from environment variables. Production boot stops when a required setting is missing.
These are the variables you'll set:
| Variable | Required | What it does |
|---|---|---|
APP_HOST | Yes | Public hostname, no protocol. Used in links and host authorization. |
ACTIVE_STORAGE_SERVICE | Yes | local or s3. |
RAILS_MASTER_KEY | Yes | Decrypts the credentials file. Unique per installation. |
MAIL_FROM | Yes | Sender address on Campsend email. |
SMTP_ADDRESS | Yes | SMTP server hostname. |
SMTP_USERNAME | Yes | SMTP username. |
SMTP_PASSWORD | Yes | SMTP password. |
SMTP_PORT | No | Defaults to 587. |
SOLID_QUEUE_IN_PUMA | No | Runs the job queue inside the web process. Right for one host. |
WEB_CONCURRENCY | No | Puma processes. Defaults to 1. |
JOB_CONCURRENCY | No | Solid Queue workers. Defaults to 1. |
RAILS_LOG_LEVEL | No | Defaults to info. |
Storage variables are in Choosing where files live. The repository has the full reference, including every S3 and Google Drive setting.
Read the configuration reference
Fixed limits
These are set in the code and are the same in every installation.
| Sign-in link | 15 minutes, single use |
|---|---|
| Sender session | 30 days |
| Delivery link | 30 days |
| Signed storage URL | 5 minutes |
| Files in one delivery | 20 |
| Size of one delivery | 2 GB |
Enabling Google Drive imports
Google Drive imports are off unless you set all three variables. Leave them unset and nothing about the feature appears in the interface.
To turn it on, create a Google Cloud project, enable the Picker and Drive APIs, then configure the consent screen with the drive.file scope. Create an OAuth web client that lists your Campsend origin, and an API key restricted to that origin and the Picker API. Then set GOOGLE_DRIVE_CLIENT_ID, GOOGLE_DRIVE_API_KEY and GOOGLE_DRIVE_APP_ID.
All three are browser-public configuration. You restrict them in Google Cloud rather than treating them as server secrets, so don't add a client secret. Campsend uses a short-lived browser token for each import and stores no Google refresh token, so it reads only the files someone picks in Picker.
What you get
A self-hosted Campsend has no storage limit and no delivery limit. Plans don't exist in this code. Quotas and paid tiers live in a separate distribution that extends Campsend through one hook, so nothing here is turned off to sell you an upgrade.
You get delivery branding, return uploads from recipients, scheduled deliveries, file revisions, collections and open and download tracking.
The hook is CAMPSEND_EXTENSIONS_GEMFILE, which loads an extra Gemfile at bundle time. Extensions run with the same privileges as Campsend, so only load code you trust.
Questions
-
Is SQLite enough?
For one host, yes. A local stress test held 670 to 860 requests a second on reads, with p99 under 200ms at 100 concurrent connections, and ran 37,000 requests over 60 seconds with no failures. Ruby rendering reaches its limit before SQLite does. Move to a server database when you need a second application host.
-
Can I run more than one web host?
Not against the same SQLite volume. Split Solid Queue into its own process and move to a server database first.
-
How do I move files from local disk to a bucket?
Changing
ACTIVE_STORAGE_SERVICEdoesn't move anything, so files stored under the old service stop resolving. Read the security and storage explanation in the repository before you migrate objects. -
Do I need libvips or a JavaScript build step?
No. Campsend uses importmaps, so there's no bundler to run, and image variants are turned off.
-
Something's wrong with the docs or the code.
Open an issue on the repository. Pull requests are welcome.