Skip to content
This repository was archived by the owner on Jul 16, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ POSTGRES_PASSWORD=bufferdash_password
# Auth
SESSION_SECRET=change_this_to_a_long_random_secret
ADMIN_EMAIL=admin@example.com
# Prefer ADMIN_PASSWORD_HASH. Generate with: node -e "const bcrypt=require('bcryptjs'); bcrypt.hash(process.argv[1], 12).then(console.log)" 'your-password'
# Prefer ADMIN_PASSWORD_HASH. See README for an interactive generation command.
ADMIN_PASSWORD_HASH=
# Development fallback only. Do not use plain-text passwords in production.
ADMIN_PASSWORD=change_this_password
Expand All @@ -25,11 +25,22 @@ ANONYMIZE_IP=true
TRUST_PROXY=true
ENFORCE_TRACKING_ORIGIN=true

# GeoIP (optional). Trusted Cloudflare/Vercel headers work without a token.
# Lite provides country/ASN; Core also provides city/region.
IPINFO_TOKEN=
IPINFO_TIER=lite

# Optional structured events from a trusted reverse proxy or host security agent.
ENABLE_LOG_INGESTION=false
INGESTION_SECRET=

# Security
RATE_LIMIT_TRACKING_PER_MINUTE=120
RATE_LIMIT_ADMIN_PER_MINUTE=60

# Server Monitoring Optional
ENABLE_SERVER_METRICS=false
METRICS_INTERVAL_SECONDS=60
CLEANUP_INTERVAL_HOURS=24
DATA_RETENTION_DAYS=90
FILTER_BOTS=false
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,31 @@ on:
jobs:
validate:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: bufferdash_test
POSTGRES_USER: bufferdash
POSTGRES_PASSWORD: test_password
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U bufferdash -d bufferdash_test"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgresql://bufferdash:test_password@127.0.0.1:5432/bufferdash_test
TEST_DATABASE_URL: postgresql://bufferdash:test_password@127.0.0.1:5432/bufferdash_test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx prisma migrate deploy
- run: npm run lint
- run: npm run typecheck
- run: npm test
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ node_modules/
.next/
dist/
coverage/
output/playwright/
.playwright-cli/
prisma/dev.db
tsconfig.tsbuildinfo
bufferdash_build_plan.md
22 changes: 22 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Contributing

## Development

Run PostgreSQL, copy `.env.example` to `.env`, and use non-production development values.

```bash
npm ci
npx prisma migrate dev
npm run dev
```

Before submitting a change:

```bash
npm run validate
docker compose config --quiet
```

Changes to `prisma/schema.prisma` must include a reviewed migration. Security-sensitive changes should include focused tests. Never commit `.env`, logs, database dumps, visitor data, production hostnames, or credentials.

Keep public ingestion endpoints small and bounded. Analytics reads and operational mutations must remain authenticated and server-side.
75 changes: 75 additions & 0 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Production Deployment

## Prerequisites

- A Linux VPS with current security updates
- Docker Engine with the Compose plugin
- A DNS record such as `dash.example.com` pointing to the VPS
- Caddy or Nginx terminating HTTPS

## Configure

```bash
git clone https://github.com/1337lean/bufferdash.git
cd bufferdash
cp .env.example .env
chmod 600 .env
```

Generate separate database, session, tracking, and optional ingestion secrets. Never reuse a secret and never commit `.env`.

Use a hexadecimal database password so it can safely appear in `DATABASE_URL`. Keep the bcrypt admin hash single-quoted in `.env` so its `$` characters remain literal.

Recommended privacy defaults:

```env
ANONYMIZE_IP=true
TRUST_PROXY=true
ENFORCE_TRACKING_ORIGIN=true
FILTER_BOTS=false
ENABLE_SERVER_METRICS=true
DATA_RETENTION_DAYS=90
```

## Start and verify

```bash
docker compose up -d --build
docker compose ps
curl -fsS http://127.0.0.1:3000/health
```

Both `app` and `worker` should become healthy, `migrate` should exit successfully, and `postgres` should remain healthy. BufferDash binds only to `127.0.0.1:3000`; PostgreSQL has no host port.

## Caddy

```caddy
dash.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:3000
}
```

Allow only SSH, HTTP, and HTTPS through the VPS firewall. If Cloudflare proxies the hostname, enable authenticated origin pulls or restrict ports 80/443 to Cloudflare's published ranges where operationally practical.

## Backups and updates

Schedule `npm run db:backup` and copy encrypted backups off the VPS. Test restoration periodically.

```bash
git pull --ff-only
docker compose up -d --build
docker compose ps
```

Database migrations run before the updated application and worker start.

## Final checks

- Log in and log out successfully over HTTPS.
- Confirm `/dashboard`, `/logs`, and `/security` redirect when logged out.
- Create a site and confirm its exact domain is configured.
- Install the snippet and check a pageview appears.
- Confirm query strings containing test values do not appear in the event stream.
- Confirm PostgreSQL and port 3000 are unreachable from the public internet.
- Confirm backups exist off-host and can be restored.
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ COPY package*.json ./
COPY prisma ./prisma
CMD ["npm", "run", "prisma:deploy"]

FROM node:22-alpine AS worker
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY package*.json ./
COPY prisma ./prisma
COPY scripts/background-worker.mjs ./scripts/background-worker.mjs
RUN npx prisma generate
CMD ["node", "scripts/background-worker.mjs"]

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
Expand Down
63 changes: 50 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ BufferDash is a self-hosted, first-party web analytics dashboard with traffic-qu

- Multi-site tracking with public site keys
- Tiny public `/tracker.js` script
- Page views, sessions, unique visitors, referrers, browsers, OS, devices, and live visitors
- Page views, sessions, bounce rate, unique visitors, referrers, browsers, OS, devices, locations, and live visitors
- Selectable 24-hour, 7-day, 30-day, and 90-day analytics ranges
- Secure IP handling with optional anonymization and hashed IPs
- Bot and unusual-path signals visible to the browser tracker
- Bot, unknown-path, failed-login, and rate-limit security signals
- Optional structured SSH, Fail2Ban, and reverse-proxy event ingestion
- Protected admin dashboard with signed HTTP-only sessions and CSRF checks for UI mutations
- Optional metrics visible to the BufferDash process, clearly identified as container/runtime data when applicable
- Background retention cleanup and optional one-minute runtime metric collection
- Docker Compose setup with PostgreSQL

## Quick Start
Expand Down Expand Up @@ -38,7 +40,8 @@ http://localhost:3000

```bash
cp .env.example .env
docker compose up -d
# Replace every production placeholder in .env first.
docker compose up -d --build
```

The Compose stack starts PostgreSQL, waits for it to become healthy, runs Prisma migrations with the `migrate` service, and then starts the app. PostgreSQL is persisted in the `postgres_data` Docker volume and is not published on a host port. BufferDash binds only to `127.0.0.1:3000` by default.
Expand Down Expand Up @@ -69,13 +72,19 @@ ADMIN_PASSWORD_HASH=replace_with_a_bcrypt_hash
TRUST_PROXY=true
ANONYMIZE_IP=true
ENABLE_SERVER_METRICS=false
IPINFO_TOKEN=
IPINFO_TIER=lite
ENABLE_LOG_INGESTION=false
INGESTION_SECRET=
```

Generate secrets and the admin password hash:
Generate secrets and the admin password hash. Generate the hash on a trusted machine after `npm ci`; the interactive form keeps the password out of shell history:

```bash
openssl rand -base64 48
node -e "const bcrypt=require('bcryptjs'); bcrypt.hash(process.argv[1], 12).then(console.log)" 'your-password'
read -s ADMIN_PASSWORD; export ADMIN_PASSWORD
node -e 'require("bcryptjs").hash(process.env.ADMIN_PASSWORD, 12).then(console.log)'
unset ADMIN_PASSWORD
```

Start or update the app:
Expand Down Expand Up @@ -118,10 +127,12 @@ ADMIN_PASSWORD_HASH=
SESSION_SECRET=replace_with_a_long_random_secret
```

Generate a bcrypt password hash:
Generate a bcrypt password hash without putting the password in shell history:

```bash
node -e "const bcrypt=require('bcryptjs'); bcrypt.hash(process.argv[1], 12).then(console.log)" 'your-password'
read -s ADMIN_PASSWORD; export ADMIN_PASSWORD
node -e 'require("bcryptjs").hash(process.env.ADMIN_PASSWORD, 12).then(console.log)'
unset ADMIN_PASSWORD
```

For local development only, `ADMIN_PASSWORD` can be used as a fallback.
Expand All @@ -142,7 +153,26 @@ window.bufferdash.track("tool_used", {
});
```

The tracker does not collect form inputs, cookies, localStorage contents, passwords, or URL fragments.
The tracker does not collect form inputs, cookies, localStorage contents, passwords, URL fragments, or query strings by default. Add `data-include-query` only when you have audited every tracked URL and intentionally want query-string analytics.

## GeoIP

When `TRUST_PROXY=true`, BufferDash uses trusted Cloudflare or Vercel location headers when present. On a normal VPS, set `IPINFO_TOKEN` to enable server-side enrichment. `IPINFO_TIER=lite` provides country and ASN data; `core` provides city and region as well. IP lookups are cached for six hours and private network addresses are never sent to the provider.

GeoIP sends a visitor IP to the configured provider. Leave `IPINFO_TOKEN` empty if that does not fit your privacy policy.

## Optional Host Security Events

Set `ENABLE_LOG_INGESTION=true` and generate a distinct `INGESTION_SECRET` of at least 32 characters. Trusted host tooling can then send a single structured event or a batch of up to 50 events:

```bash
curl -fsS -X POST https://dash.example.com/api/security/ingest \
-H "Authorization: Bearer $INGESTION_SECRET" \
-H "Content-Type: application/json" \
--data '{"source":"fail2ban","type":"ban","message":"Banned repeated SSH failures","ip":"203.0.113.10"}'
```

Keep ingestion disabled unless you actively use it. The endpoint returns `404` when disabled or unauthorized.

## Environment Variables

Expand All @@ -159,6 +189,9 @@ See `.env.example` for the full set. The most important production values are:
- `ANONYMIZE_IP`
- `TRUST_PROXY`
- `ENFORCE_TRACKING_ORIGIN`
- `IPINFO_TOKEN` and `IPINFO_TIER` (optional)
- `ENABLE_LOG_INGESTION` and `INGESTION_SECRET` (optional)
- `METRICS_INTERVAL_SECONDS` and `CLEANUP_INTERVAL_HOURS`

Settings are environment-driven in v1 so secrets and operational toggles are not exposed through a browser editor.

Expand All @@ -168,6 +201,8 @@ BufferDash can log IP addresses and user agents. If you deploy it, disclose anal

Data retention cleanup is available from `/settings`. It removes old events, sessions, orphaned visitor identifiers, traffic flags, and runtime metrics. The default retention window is controlled by `DATA_RETENTION_DAYS`.

The background worker performs this cleanup automatically and has its own Docker health check. The manual settings action remains available for immediate cleanup.

## Security Notes

- `.env` is ignored by Git.
Expand All @@ -176,6 +211,7 @@ Data retention cleanup is available from `/settings`. It removes old events, ses
- Production rejects placeholder secrets, non-HTTPS `APP_URL` values, and missing bcrypt admin hashes.
- `/api/track` validates payloads with Zod and rate limits by IP.
- Tracking requests are restricted to each site's configured domain by default. This limits accidental or casual key reuse, though browser origin headers are not a substitute for a private credential.
- Query strings and fragments are excluded from tracked URLs by default.
- Public APIs never return analytics data.
- Client-submitted IP, country, city, browser, OS, and device values are not trusted.
- v1 intentionally does not include a browser terminal, arbitrary file browser, or `.env` editor.
Expand All @@ -200,13 +236,14 @@ server {

## Roadmap

- GeoIP enrichment with IPinfo or MaxMind
- Optional reverse-proxy, Fail2Ban, and SSH log ingestion with a dedicated least-privilege agent
- Offline MaxMind GeoIP database support
- Packaged least-privilege host agents for common SSH and reverse-proxy formats
- User roles and TOTP
- Read-only dashboards
- Scheduled uptime, latency, HTTP status, and TLS-expiry monitoring
- Background runtime metric and retention workers
- Public screenshots and deployment guides
- Public screenshots

See [DEPLOYMENT.md](DEPLOYMENT.md) for the production checklist, [SECURITY.md](SECURITY.md) for reporting and operational boundaries, and [CONTRIBUTING.md](CONTRIBUTING.md) for development guidance.

## License

Expand Down
17 changes: 17 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Security Policy

## Reporting

Please report suspected vulnerabilities privately to the repository owner rather than opening a public issue. Include affected versions, reproduction steps, impact, and any suggested mitigation. Do not include real visitor data or credentials.

## Operational boundaries

- The public site key is an ingestion identifier, not a credential. Origin checks reduce casual misuse but cannot stop forged server-side requests.
- Keep the dashboard behind HTTPS and use a long, unique admin password. An additional access layer such as a VPN or identity-aware proxy is recommended for high-risk deployments.
- PostgreSQL must remain on the internal Docker network. Port 3000 must remain bound to localhost.
- `TRUST_PROXY=true` is safe only when the reverse proxy overwrites forwarding headers and the app is not directly reachable.
- GeoIP is optional because it sends visitor IP addresses to the configured provider.
- Host log ingestion is disabled by default and requires a separate random bearer secret.
- Runtime metrics describe what the container can observe and are not a substitute for full VPS monitoring.

If any credential is committed or exposed, remove it from use and rotate it immediately. Rewriting Git history or making the repository private is not sufficient.
Loading
Loading