# TripCircle: complete install guide (cPanel · Node.js · PostgreSQL)

**Better trips. Together.**
This guide installs TripCircle on your own cPanel hosting from the ready-made package **`tripcircle-cpanel.zip`**. Everything runs on your server: the app, the PostgreSQL database, sign-in, invitations, emails and photo uploads. No Supabase, Vercel or Mapbox account is needed.

| | |
| --- | --- |
| **Time** | About 30 minutes for a first install, 5 minutes to upgrade |
| **You need** | cPanel with *Setup Node.js App*, PostgreSQL 13+, a domain or subdomain with SSL, and ideally an email account and Terminal |
| **Package** | `tripcircle-cpanel.zip`, ready to run, with no build step on the server |

**Contents:** [1 · Before you start](#1-before-you-start) · [2 · Database](#2-create-the-database-and-two-users) · [3 · Node.js app](#3-create-the-nodejs-app) · [4 · Upload](#4-upload-and-extract-the-package) · [5 · Database setup](#5-set-up-the-database) · [6 · Start and check](#6-start-tripcircle-and-check-it-works) · [7 · Optional extras](#7-optional-extras) · [8 · Upgrading](#8-upgrading-from-an-earlier-version) · [9 · Backups](#9-backups) · [10 · Troubleshooting](#10-troubleshooting) · [11 · Security](#11-how-your-data-is-protected) · [12 · What's new](#12-whats-new-in-this-version)

---

## 1. Before you start

Check these in cPanel:

| Requirement | Where to check |
| --- | --- |
| **Setup Node.js App** (CloudLinux / Passenger) | cPanel home → *Software* |
| **Node.js 20 or newer** in that tool | The version dropdown when you create an app |
| **PostgreSQL 13 or newer** | cPanel → *phpPgAdmin* → SQL → `select version();` |
| A **domain or subdomain with SSL**, e.g. `trips.example.com` | cPanel → *Domains* and *SSL/TLS Status* (AutoSSL). Use the root of a domain or subdomain, **not** a sub-folder like `example.com/trips` |
| An **email account** to send from (recommended), e.g. `no-reply@example.com` | cPanel → *Email Accounts* |
| **Terminal** or SSH (recommended) | cPanel → *Advanced* → *Terminal*. Without it, use [the phpPgAdmin route](#5b-without-terminal-phppgadmin) |

What's inside `tripcircle-cpanel.zip`:

```text
server.js                  ← the app's start file
.next/  node_modules/      ← the built app and exactly the libraries it needs
database/                  ← SQL for the database (incl. setup-all-in-one.sql)
scripts/db-setup.cjs       ← creates/updates the database
scripts/seed.cjs           ← optional Mallorca demo TripCircle
uploads/                   ← default photo folder (we'll point UPLOAD_DIR elsewhere)
INSTALL.md                 ← this guide
.env.example               ← template for the setup scripts
```

---

## 2. Create the database and two users

TripCircle connects with **two database users**. This is what makes every TripCircle private:

- the **owner** user creates the tables. It's only used during install and upgrades.
- the **app** user is what the website uses. Because it doesn't own the tables, PostgreSQL's Row Level Security checks every query, so people only ever see TripCircles they were invited to.

In **cPanel → PostgreSQL Databases**:

1. **Create New Database**, e.g. `tripcircle` (cPanel names it `CPANELUSER_tripcircle`).
2. **Add New User**, e.g. `tcowner` (it becomes `CPANELUSER_tcowner`), with a strong password.
3. **Add New User**, e.g. `tcapp` (it becomes `CPANELUSER_tcapp`), with a **different** strong password.
4. **Add User to Database**: add **both** users to the database, one at a time.

Now write your two connection strings:

```text
Owner (setup only):  postgresql://CPANELUSER_tcowner:OWNER_PASSWORD@localhost:5432/CPANELUSER_tripcircle
App (the website):   postgresql://CPANELUSER_tcapp:APP_PASSWORD@localhost:5432/CPANELUSER_tripcircle
```

> **Passwords with special characters** must be URL-encoded inside these strings: `#`→`%23`, `@`→`%40`, `:`→`%3A`, `/`→`%2F`, `?`→`%3F`, `&`→`%26`, `%`→`%25`, space→`%20`. Example: `My#Pass@1` becomes `My%23Pass%401`.

---

## 3. Create the Node.js app

**cPanel → Setup Node.js App → Create Application**

| Field | Value |
| --- | --- |
| Node.js version | **20** or newer |
| Application mode | **Production** |
| Application root | `tripcircle` (a folder in your home directory) |
| Application URL | your domain or subdomain, e.g. `trips.example.com`, with the path **empty** |
| Application startup file | `server.js` |

Under **Environment variables**, add each of these (**Add Variable**):

| Name | Value | Required |
| --- | --- | --- |
| `NODE_ENV` | `production` | ✅ |
| `DATABASE_URL` | the **app** connection string from step 2 | ✅ |
| `SITE_URL` | `https://trips.example.com` (your address, no trailing slash) | ✅ |
| `UPLOAD_DIR` | `/home/CPANELUSER/tripcircle-uploads` (outside the app folder, so photos survive upgrades) | ✅ |
| `SMTP_HOST` | `mail.example.com` (*Email Accounts → Connect Devices* shows it) | Recommended |
| `SMTP_PORT` | `465` (or `587` if your host requires it) | Recommended |
| `SMTP_USER` | `no-reply@example.com` | Recommended |
| `SMTP_PASS` | that mailbox's password | Recommended |
| `SMTP_FROM` | `TripCircle <no-reply@example.com>` | Recommended |
| `MAPBOX_SEARCH_TOKEN` | a Mapbox token for better place search | Optional |
| `AUTO_CONFIRM_EMAILS` | `true` skips email confirmation (only if you can't send email) | Optional |

Don't add the owner connection string here, because the website never needs it. Then click **Create**, and **Stop App** for now.

> ⚠️ **Don't click "Run NPM Install".** The package already contains everything it needs.

---

## 4. Upload and extract the package

1. **cPanel → File Manager** → open your home directory → `tripcircle/`. cPanel may have put a placeholder `server.js` there; it will be replaced.
2. **Upload** `tripcircle-cpanel.zip` into `tripcircle/`.
3. Right-click the zip → **Extract** → into `/home/CPANELUSER/tripcircle`. Allow it to overwrite, then delete the zip.
4. Create the photo folder you chose for `UPLOAD_DIR`, e.g. `/home/CPANELUSER/tripcircle-uploads`.

Turn on **Settings → Show Hidden Files** in File Manager. You should see `server.js`, `.next`, `node_modules`, `database` and `scripts` in `tripcircle/`.

---

## 5. Set up the database

### 5a. With Terminal (recommended)

1. In **Setup Node.js App**, open your app. At the top is a command such as
   `source /home/CPANELUSER/nodevenv/tripcircle/20/bin/activate && cd /home/CPANELUSER/tripcircle`.
   Copy it, paste it into **cPanel → Terminal** and press Enter.
2. Create a temporary `.env` with **both** connection strings (paste your real values):

   ```bash
   cat > .env <<'EOF'
   DATABASE_URL=postgresql://CPANELUSER_tcapp:APP_PASSWORD@localhost:5432/CPANELUSER_tripcircle
   MIGRATION_DATABASE_URL=postgresql://CPANELUSER_tcowner:OWNER_PASSWORD@localhost:5432/CPANELUSER_tripcircle
   EOF
   ```

3. Create the tables, privacy rules and permissions:

   ```bash
   node scripts/db-setup.cjs
   ```

   ✅ You should see a ✓ for each SQL file and then
   `granted app access to CPANELUSER_tcapp (Row Level Security enforced)`.

4. *(Optional)* Load the **Mallorca demo TripCircle**: 5 demo friends, password `tripcircle-demo`, signing in as `emma@tripcircle.demo` and so on:

   ```bash
   node scripts/seed.cjs
   node scripts/seed.cjs --member you@example.com   # after you've signed up: adds you to the demo
   ```

   ⚠️ The demo includes a reusable invite link, `/invite/mallorca-2027-demo`. On a public site, revoke it in the app (**+ / Invite friends → Revoke**) or skip the demo.

5. Remove the owner password from the server:

   ```bash
   rm .env
   ```

### 5b. Without Terminal (phpPgAdmin)

1. In **File Manager**, open `tripcircle/database/setup-all-in-one.sql` with **Edit**, use *Search & Replace* to change every `APP_USER_NAME` to your app user (e.g. `CPANELUSER_tcapp`), and save.
2. Open **cPanel → phpPgAdmin**, log in as the **owner** user, select your database → **SQL** → paste the whole file → **Execute**.

The demo data needs Terminal; without it, just create your first TripCircle in the app.

---

## 6. Start TripCircle and check it works

1. **Setup Node.js App** → your app → **Start App**, or **Restart** if it's already running.
2. Open `https://trips.example.com`. You should see **Better trips. Together.**
3. **Sign up**, then click the link in the confirmation email. Without SMTP, the link is written to `tripcircle/stderr.log`.
4. Click **New TripCircle**, fill in the trip, and invite your friends from the window that opens. The 2-minute **guided tour** starts automatically on your first visit; replay it any time with **?**.

### Five-minute check

| Do this | You should see |
| --- | --- |
| Open `https://trips.example.com/circles` in a private window | The log-in page |
| Sign up a second account and open your TripCircle's link with it | "This TripCircle is private" |
| Invite that account's email, open the invite and join | It can now see the TripCircle |
| Add a place in one browser | It appears in the other within ~3 seconds |
| Remove a place, then click **Undo** in the message that pops up | The place comes back |
| Upload a profile photo, then **Restart** the app | The photo is still there |

---

## 7. Optional extras

These need a **custom build**, because they're compiled into the app. On your own computer (Node.js 20+ and Git):

```bash
git clone https://github.com/rajkaul86/google-map-collab.git tripcircle && cd tripcircle
npm install
```

Create `.env.production` with what you want:

```ini
NEXT_PUBLIC_BACKEND=selfhost
NEXT_PUBLIC_SITE_URL=https://trips.example.com
# Mapbox maps (nicer satellite/street maps). Use a public "pk." token, URL-restricted to your domain:
NEXT_PUBLIC_MAPBOX_TOKEN=pk.xxxxx
# One-click demo friends on the log-in page (good for showing people around):
NEXT_PUBLIC_SHOW_DEMO_LOGINS=true
```

Then run `npm run package:cpanel` and upload the new `deploy/tripcircle-cpanel.zip` as in [Upgrading](#8-upgrading-from-an-earlier-version).

Without these, TripCircle uses free OpenStreetMap-based maps (OpenFreeMap streets and Esri satellite imagery) and OpenStreetMap search.

---

## 8. Upgrading from an earlier version

Your TripCircles, accounts and photos are kept. Nothing in the database is deleted.

1. **Setup Node.js App** → **Stop App**.
2. *(Recommended)* back up first ([§9](#9-backups)).
3. **File Manager** → `tripcircle/` → upload the new `tripcircle-cpanel.zip` → **Extract**, allowing it to **overwrite** existing files. Delete the zip.
4. If you set up before this version used `SITE_URL`: in your app's **Environment variables**, add `SITE_URL` with your address. The older `NEXT_PUBLIC_SITE_URL` variable can stay or be removed.
5. Update the database: in **Terminal**, repeat [§5a](#5a-with-terminal-recommended) steps 1–3 and 5 (create `.env`, run `node scripts/db-setup.cjs`, then `rm .env`). Only new changes are applied; files that already ran are listed as *already applied*.
   > This version adds **no** database changes, but running the script is always safe and keeps permissions correct.
6. **Start App**. Visitors may need to refresh once to pick up the new version.

---

## 9. Backups

| What | How |
| --- | --- |
| Database | cPanel → *Backup* → **PostgreSQL Databases** → download, or rely on your host's automatic backups |
| Photos | File Manager → select your `UPLOAD_DIR` folder → **Compress** → download |
| Settings | Copy your app's environment variables somewhere safe (a password manager is ideal) |

To restore: upload the database backup through phpPgAdmin (or ask your host), extract the photos back into `UPLOAD_DIR`, and re-enter the environment variables.

---

## 10. Troubleshooting

Most errors are written to **`tripcircle/stderr.log`**. Open it in File Manager first.

| Problem | Fix |
| --- | --- |
| *"Incomplete response received from application"* or 503 | Check `stderr.log`. Usually `DATABASE_URL` is missing or wrong, or the app started before [§5](#5-set-up-the-database). Fix it, then **Restart**. |
| `password authentication failed` | User names include the cPanel prefix (`CPANELUSER_…`); special characters in passwords must be URL-encoded ([§2](#2-create-the-database-and-two-users)). |
| `DATABASE_URL and MIGRATION_DATABASE_URL use the same database user` | Create the second (app) user. Using one user would switch off the privacy protection. |
| `PostgreSQL 13 or newer is required` | Ask your host to upgrade PostgreSQL. |
| `permission denied for table …` | Run `node scripts/db-setup.cjs` again; it re-applies permissions. |
| Pages load but look unstyled | `.next/static` wasn't extracted. Show hidden files and extract the zip again. |
| No emails arrive | Check the `SMTP_*` variables (try port 587 if 465 fails). Until it works, confirmation and reset links appear in `stderr.log`. Set up SPF and DKIM in cPanel → *Email Deliverability* so mail isn't marked as spam. |
| Email links point to the wrong address | Set `SITE_URL` to your exact address (with `https://`, no trailing slash) and **Restart**. |
| *"Please verify your email address before accepting this invitation"* | Personal invites need a confirmed email. Use the confirmation link (or **Profile → Resend email**). Group invite links work without it. |
| *"This TripCircle is private"* for someone who should have access | They need to accept an invitation first; check the member list in **+ / Invite friends**. |
| Map background is blank | The free map tile servers are blocked on that network. Do a custom build with a Mapbox token ([§7](#7-optional-extras)). |
| Photos vanish after an upgrade | `UPLOAD_DIR` wasn't set, so photos were inside the app folder. Set it to a folder outside `tripcircle/` and move the old `tripcircle/uploads/*` there. |
| Changes from friends take a few seconds | That's normal on cPanel: the app checks for updates every ~2.5 seconds. |
| The guided tour doesn't start | It only starts on someone's first visit. Use the **?** button at the top to replay it. |

---

## 11. How your data is protected

- **Private TripCircles.** Every page and API request checks membership on the server. In the database, Row Level Security enforces the same rule, and the app's database user can't switch it off.
- **Invitations.** Group links or personal invites. Personal invites work only for that **verified** email address. Links are long random tokens that expire after 14 days and can be revoked.
- **Accounts.** Passwords are hashed with scrypt. Sessions are HTTP-only, SameSite cookies, Secure on HTTPS, and only a hash of each token is stored. Sign-in and reset requests are rate-limited, and resetting a password signs out other sessions.
- **Uploads.** Only JPEG, PNG, WebP or GIF are accepted, limited to 5 MB for photos and 2 MB for avatars. Files get random names and are served with `nosniff`.
- **Private pages** are sent with `noindex`, `no-referrer` and `no-store` headers. Requests from other websites that try to change data are blocked.

---

## 12. What's new in this version

**Easier to read and use**
- Higher-contrast text: secondary text now passes WCAG AA, and category labels use darker, accessible colours.
- No body text smaller than 12px. Phones get 44px touch targets everywhere.
- Clear keyboard focus outlines, and animations switch off for people who prefer reduced motion.

**Clearer layout**
- One filter system: category chips on the map, plus the filter button and a "filtered by…" summary in the list.
- Roomier place cards with two-line descriptions. The ⋯ menu appears on hover, and hovering a card highlights its pin.
- While a place is open, Group activity shrinks to a single "Latest activity" line.
- Nearby pins group into numbered clusters at low zoom, and other pins dim when one is selected. The app remembers each person's map position and style.
- The ideas tray in the itinerary wraps into a grid and each idea has an **Add to day** button, so you don't have to drag. The Ideas cards are calmer.
- Budget figures carry equal weight. "Make final" on polls appears on hover, or in the poll's ⋯ menu on phones.

**Faster and friendlier**
- **Undo** after removing a place, comment, activity, expense or poll, and after taking back "I'm in". This replaces most confirmation dialogs.
- Votes, saves, comments and poll votes appear instantly.
- One voting language everywhere: 👍 **"I'm in"**.
- The **Add place** form asks only for location, name and category, with everything else under **More details**.
- New TripCircles get a **Get your TripCircle going** checklist, and returning friends see **Since your last visit**.
- **"Liam is typing…"** in comment threads, and a dot on **Decisions** when a poll needs your vote.
- Pages load with a layout-shaped preview instead of a spinner. Toasts appear at the top so they don't cover what you're doing.

**Phones**
- The bottom bar has five tabs (Circle, Map, Plan, Money, Votes); Ideas is reachable from the Places list and the Circle page.
- Place details open in a sheet you can drag between peek, half and full.
- Map controls make room for the sheet; on touch screens, pinch to zoom.
