> ## Documentation Index
> Fetch the complete documentation index at: https://skaum.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Development Setup

> Get your local development environment up and running

## Prerequisites

* **Ruby** 3.4+ (see `.ruby-version`)
* **SQLite3** development libraries
* **Node.js** (for Tailwind CSS build)

## Installation

Clone the repository and install dependencies:

```bash theme={null}
git clone <repo-url>
cd voteringsapp
bundle install
```

Set up the database:

```bash theme={null}
bin/rails db:setup
```

This creates the SQLite database, runs all migrations, and seeds initial data.

## Running the App

```bash theme={null}
bin/dev
```

This starts two processes via `Procfile.dev`:

* **Puma** web server on `http://localhost:3000`
* **Tailwind CSS** watcher for live stylesheet compilation

## Reset and Re-seed

To reset the database to a clean state with fresh seed data:

```bash theme={null}
bin/rails db:reset
```

This drops the database, re-runs all migrations, and re-loads all seed data.

After resetting, you can log in as:

| Email | Role in all meetings |
| - | - |
| `chair@example.com` | Møteleder |
| `secretary@example.com` | Sekretær |
| `participant01@example.com` … `participant48@example.com` | Deltaker |

The seed database contains one organization (**Sameiet Eiketoppen**) with 10 meetings across all statuses, complete with agenda items, polls, and votes.

## Key Commands

| Command | Description |
| - | - |
| `bin/dev` | Start development server |
| `bin/rails db:reset` | Drop, migrate, and re-seed the database |
| `bin/rails test` | Run all unit and integration tests |
| `bin/rails test:system` | Run system tests (Capybara + Selenium) |
| `bin/ci` | Full CI check: tests + RuboCop + Brakeman + bundler-audit |
| `bin/rubocop` | Run RuboCop linter |
| `bin/brakeman` | Run Brakeman security scanner |
| `bin/bundler-audit` | Audit gems for known vulnerabilities |

## Email in Development

The app uses [letter\_opener](https://github.com/ryanb/letter_opener) in development. Magic link emails open automatically in your browser instead of being sent.

## Environment Variables

Secrets are managed via [Doppler](https://doppler.com). After cloning the repo, link your local environment:

```bash theme={null}
doppler login   # authenticate (first time only)
doppler setup   # select project "votering" and config "dev_personal"
```

Once linked, `bin/dev` injects secrets automatically — no `.env` file needed.

| Secret | Environments | Purpose |
| - | - | - |
| `RAILS_MASTER_KEY` | all | Decrypts `config/credentials.yml.enc` |
| `MAILTRAP_API_KEY` | stg | Mailtrap API authentication (staging) |
| `MAILTRAP_INBOX_ID` | stg | Mailtrap sandbox inbox ID (staging) |
| `RESEND_API_KEY` | prd | Email delivery on production |
| `APP_HOST` | stg, prd | Mailer URL generation |
| `ADMIN_EMAIL` | all | Account promoted by `bin/rails admin:bootstrap` |
| `ADMIN_ALLOWED_IPS` | stg, prd | Comma-separated IP allowlist for `/admin` |
| `STRIPE_SECRET_KEY` | all | Stripe API key (Checkout, Customer Portal) |
| `STRIPE_WEBHOOK_SECRET` | all | Verifies Stripe webhook signatures |
| `SENTRY_DSN` | stg, prd | Sentry error monitoring DSN (Sentry is inert if unset) |
| `LITESTREAM_REPLICA_BUCKET` | stg, prd | Litestream backup target (Litestream is inert if unset) |
| `LITESTREAM_ACCESS_KEY_ID` | stg, prd | Object-storage access key for Litestream |
| `LITESTREAM_SECRET_ACCESS_KEY` | stg, prd | Object-storage secret key for Litestream |
| `LITESTREAM_REPLICA_ENDPOINT` | stg, prd | Optional — only for non-AWS S3 providers |
| `LITESTREAM_REPLICA_REGION` | stg, prd | Optional — AWS S3 / Backblaze B2 region |
| `TURNSTILE_SITE_KEY` | stg, prd | Cloudflare Turnstile public key for signup |
| `TURNSTILE_SECRET_KEY` | stg, prd | Cloudflare Turnstile secret key for signup |
| `DOCS_URL` | all | Base URL of the user guide (`https://skaum.mintlify.site/`); the «Brukerveiledning» and guide links render only when it is set (`DocsLink`) |
| `KAMAL_REGISTRY_PASSWORD` | prd | Container registry password for `kamal deploy` |

Turnstile and Litestream are both fully ENV-gated: Turnstile needs **both** keys
before the widget renders or a token is verified, and Litestream needs
`LITESTREAM_REPLICA_BUCKET`. Local development and test runs are unaffected by
leaving them unset. See
[Backup and Recovery](/utvikling/backup-og-gjenoppretting) for the Litestream
restore runbook.

For deployment configuration, see `config/deploy.yml` (production) and `config/deploy.staging.yml` (NAS staging).

## Staging Deployment (NAS)

The staging environment runs on a UGreen NAS at `100.65.168.27` via Tailscale. It uses Kamal 2 with Docker.

### Prerequisites

* **Tailscale** — connected to the same tailnet as the NAS
* **Doppler CLI** — authenticated (`doppler login`)
* **SSH key** — your `~/.ssh/id_ed25519.pub` must be in `~/.ssh/authorized_keys` on the NAS (user `hakon`)

### First-time setup

```bash theme={null}
KAMAL_DESTINATION=staging kamal setup --destination staging
```

This installs kamal-proxy on the NAS, builds the Docker image locally, pushes it to the local registry, and starts the app container. After setup, prepare the database:

```bash theme={null}
KAMAL_DESTINATION=staging kamal app exec --destination staging "bin/rails db:prepare"
```

### Deploying

```bash theme={null}
KAMAL_DESTINATION=staging kamal deploy --destination staging
```

Kamal reads secrets from Doppler (via `.kamal/secrets.staging`), builds the image, and deploys it. The app is accessible at `http://100.65.168.27:8080/`.

### Useful commands

| Command | Description |
| - | - |
| `KAMAL_DESTINATION=staging kamal deploy --destination staging` | Deploy latest code |
| `KAMAL_DESTINATION=staging kamal app logs --destination staging` | Tail app logs |
| `KAMAL_DESTINATION=staging kamal app exec --destination staging "bin/rails console"` | Open Rails console |
| `KAMAL_DESTINATION=staging kamal app exec --destination staging "bin/rails db:prepare"` | Run migrations |

### Architecture

* **Rails env**: `staging` (dedicated environment — production-like with debug logging and Mailtrap email)
* **Config**: `config/deploy.staging.yml` (overrides `config/deploy.yml`)
* **Secrets**: `.kamal/secrets.staging` (fetches from Doppler `stg` config)
* **Proxy**: kamal-proxy on port 8080 (HTTP) and 8443 (HTTPS) — avoids conflict with NAS nginx on 80/443
* **Storage**: SQLite databases persist in `/volume1/docker/votering/` on the NAS (bind mount to `/rails/storage`)
* **SSH user**: `hakon` (configured in `deploy.staging.yml`)

> [!NOTE]
>
> Staging uses `RAILS_ENV=staging`, so `db:seed` works. After first-time setup, run `kamal app exec --destination staging "bin/rails db:seed"` to populate test data.

### Logging in on staging

Staging uses the Mailtrap API in **sandbox mode** — emails are not delivered to real recipients. Instead, they appear in the [Mailtrap sandbox inbox](https://mailtrap.io/inboxes). Check the sandbox inbox to find magic link emails. If you need to log in without checking Mailtrap, generate a magic link URL directly from the Rails console:

```bash theme={null}
KAMAL_DESTINATION=staging kamal app exec -i --destination staging "bin/rails console"
```

Then in the console:

```ruby theme={null}
user = User.find_by(email: "chair@example.com")
token = user.generate_token_for(:magic_link)
Rails.application.routes.url_helpers.verify_session_url(token: token, host: ENV["APP_HOST"])
```

Open the resulting URL in your browser to log in. The token expires in 10 minutes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.