Files
space 90906a3c78
Deploy / Build (pull_request) Successful in 37s
Deploy / Build and Push Docker Image (pull_request) Has been skipped
fix: open preview security groups by default
Co-Authored-By: Codex <codex@openai.com>
2026-07-26 19:11:41 +02:00

26 KiB
Raw Permalink Blame History

PR Previews (PP)

PP is a self-hosted service that connects to a Gitea instance via webhook. When a PR is opened or updated, PP automatically provisions an AWS EC2 instance, builds and runs the project, and comments a live preview URL back on the PR. Designed for teams and agents iterating on code without needing local environments.

Purpose

  • Provide a shareable live preview of any PR before it's merged.
  • Remove the need for reviewers to pull and run code locally.
  • Works well in agent-driven workflows where humans review from mobile or external environments.

Tech Stack

  • Backend: rj-web from ../shsf (same setup pattern), logging + auth middleware + Prisma from ../shsf adapted to this project's needs.
  • Frontend: React + Vite + Tailwind CSS (dark/light mode toggle, responsive).
  • Database: PostgreSQL via Prisma (schema at ./prisma/schema.prisma).
  • CI/CD: Reuse ../shsf CI/CD pipeline; build and push Docker image to the same registry under /pp-previews/core.

Project Structure

./backend/             # Node backend (rj-web)
./frontend/            # React + Vite frontend
./backend/prisma/schema.prisma # Prisma schema
./docker-compose.yml   # Compose + Dockerfile for PP itself

Implementation Notes

You're building this from scratch. Refer to ../shsf for patterns on rj-web setup, logging, auth middleware, and Prisma client initialization.

Testing: No automated tests for V1. Build to fully working state locally first. The first real test will be against a live Gitea instance with a real repo. Build as if it will be tested end-to-end immediately.


Data Model (Prisma)

User

PP account.

  • id, username, passwordHash, isAdmin, isFounder, createdAt
  • giteaUsername, giteaPAT (encrypted), giteaInstanceUrl (e.g. https://gitea.example.com, no trailing slash)
  • awsAccessKeyId (encrypted), awsSecretAccessKey (encrypted), awsRegion

RepoConfig

Per-repo settings, one per user per repo. A repo can only be configured by one user — first to save wins; others receive an error.

  • id, userId, repoOwner, repoName
  • isEnabled (bool)
  • giteaWebhookId (string, stored after auto-registration so PP can delete/update it later)
  • denyList (string[], Gitea usernames to skip — e.g. dependabot)
  • instanceType (string, default from AdminSettings.defaultInstanceType)
  • inactivityHours (float, default 12, range 0.572)
  • port (int, default 3000 — the port the app listens on inside the EC2 instance)
  • envVars (JSON key-value)
  • preinstallTools (string[] — selected runtime/tooling bootstrap options: docker, node, python, go, lua, build-essential)
  • nodeVersion (string, nullable — used when node is selected; default lts/*; .nvmrc in the repo wins during deploy)
  • useDockerCompose (bool)
  • composeFilePath (string, nullable — path within repo, default docker-compose.yml)
  • aptPackages (string[])
  • setupCommands (string[])
  • buildCommands (string[])
  • postBuildCommands (string[])
  • runCommand (string, single — non-compose mode only)

Preview

One record per PR. Persists across pushes; the EC2 instance is reused on each new push (only build + run steps re-run).

  • id, repoConfigId, prNumber, prTitle
  • commitSha (updated on each push)
  • instanceId (AWS EC2 instance ID)
  • instanceIp (public IP)
  • port (copied from RepoConfig at deploy time)
  • status (enum: PROVISIONING | BUILDING | RUNNING | FAILED | STOPPED | IGNORED)
  • logs (text, appended each redeploy, capped at AdminSettings.logSizeLimitBytes — oldest lines truncated with a --- logs truncated --- marker)
  • pid (int, nullable — non-compose mode only, the background process PID on the EC2 instance)
  • sshPrivateKey (encrypted — ephemeral, generated per launch, deleted on instance termination)
  • sshKeyName (string — AWS key pair name, for deletion on terminate)
  • giteaCommentId (int, nullable — the Gitea comment ID of PP's status comment, for in-place edits)
  • lastActivityAt (DateTime — updated on each push webhook AND each PR comment webhook)
  • createdAt, updatedAt, stoppedAt

Job

Durable async job queue for deployments and stops.

  • id, previewId (nullable), type (enum: DEPLOY | STOP | INACTIVITY_STOP), status (enum: PENDING | RUNNING | DONE | FAILED)
  • payload (JSON), error (text, nullable)
  • createdAt, startedAt, finishedAt

WebhookToken

Per-user HMAC secret for verifying Gitea webhook payloads.

  • id, userId, token (random secret), createdAt

NoConfigComment

Tracks which PRs have already received a "no previews configured" comment to avoid repeat spam.

  • id, userId, repoOwner, repoName, prNumber, createdAt

AdminSettings

Single row global config (seeded on first run).

  • id
  • defaultInstanceType (string, default t3.medium)
  • maxConcurrentInstancesPerUser (int, default 5)
  • logSizeLimitBytes (int, default 1048576 — 1MB)
  • previewRetentionDays (int, default 30 — STOPPED/FAILED records older than this are purged)
  • webhookRateLimitPerMinute (int, default 10 — per user)
  • contactEmail (string — shown in privacy policy footer)

Backend

Auth

  • Session-based auth (reuse shsf middleware).
  • Registration endpoint is disabled — only admins can create users via the admin panel.
  • First registered user automatically becomes admin and is flagged as isFounder = true. The founder cannot be demoted or deleted.

Webhook Endpoint

POST /webhook/:userId

Rate limiting: Each user is limited to AdminSettings.webhookRateLimitPerMinute webhook events per minute. Excess requests receive 429 Too Many Requests and are dropped (not queued).

Signature verification: Gitea sends X-Gitea-Signature-256: sha256=<hmac>. PP verifies using the user's WebhookToken.token before doing anything else. Invalid signature → 401.

Accepted Gitea events (set via X-Gitea-Event header):

  • pull_request with action opened → trigger new deploy
  • pull_request with action synchronize → trigger redeploy on existing Preview
  • pull_request with action reopened → same as opened
  • pull_request with action closed → stop and terminate the preview
  • issue_comment with action created → parse for /pp commands (see PP Commands section)

Webhook handler returns 200 OK immediately after enqueueing a job. All processing is async.

Activity tracking: Update Preview.lastActivityAt = now on both synchronize and issue_comment events.

PP Commands

PP listens for comments on PRs (via issue_comment webhook). A comment is a PP command if it starts with /pp as the first line. Commands are only acted on if posted by: the PR author, or a Gitea user with owner or admin role on the repo (checked via GET /api/v1/repos/{owner}/{repo}/teams or collaborator endpoint).

Command Behaviour
/pp rebuild Re-runs build + start steps on the existing EC2 instance (same as a new push but without a git pull).
/pp stop Stops and terminates the preview instance. Sets status to STOPPED.
/pp start If STOPPED: re-provisions a new EC2 and deploys. If IGNORED: un-ignores AND immediately triggers a fresh deploy.
/pp logs PP posts a new comment with the last 50 lines of Preview.logs in a fenced code block.
/pp ignore Sets Preview.status = IGNORED. Future pushes and commands (except /pp start) are silently ignored. If no Preview exists yet, creates one in IGNORED state so future pushes are skipped.

PP does not react to its own comments (check that the commenter's username != User.giteaUsername for the configured user).

AWS EC2 Management

Required IAM permissions (document in the setup wizard UI with a copy-pasteable JSON policy):

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "ec2:RunInstances",
      "ec2:TerminateInstances",
      "ec2:DescribeInstances",
      "ec2:CreateSecurityGroup",
      "ec2:DeleteSecurityGroup",
      "ec2:AuthorizeSecurityGroupIngress",
      "ec2:DescribeSecurityGroups",
      "ec2:CreateKeyPair",
      "ec2:DeleteKeyPair",
      "ec2:CreateTags",
      "sts:GetCallerIdentity"
    ],
    "Resource": "*"
  }]
}

EC2 instance tagging — every instance PP launches must be tagged:

pp:managed = true
pp:userId  = <User.id>
pp:repo    = <repoOwner>/<repoName>
pp:prNumber = <prNumber>
pp:previewId = <Preview.id>

EC2 setup per preview:

  1. Generate an ephemeral RSA key pair. Store the private key encrypted in Preview.sshPrivateKey. Import the public key to AWS as pp-preview-<previewId> and store the name in Preview.sshKeyName.
  2. Create a security group named pp-preview-<previewId> in the default VPC. Allow inbound: all traffic from 0.0.0.0/0 and ::/0 (all protocols, including ICMP, and all ports).
  3. Launch instance:
    • AMI: Ubuntu 22.04 LTS (hardcode a per-region AMI map, or resolve via SSM resolve:ssm:/aws/service/canonical/ubuntu/server/22.04/stable/current/amd64/hvm/ebs-gp2/ami-id)
    • Instance type from RepoConfig.instanceType
    • Key pair: pp-preview-<previewId>
    • Security group: pp-preview-<previewId>
    • User data bootstrap script (runs once on first boot):
      #!/bin/bash
      apt-get update -y
      apt-get install -y ca-certificates curl git unzip
      
    • Repo-selected preinstall tools are installed over SSH after clone: Docker + Compose, Node via nvm, Python, Go, Lua, build tools, and custom apt packages.
    • All instance tags as above.
  4. Poll DescribeInstances until status = running and the instance has a public IP.
  5. Poll TCP port 22 until it accepts connections (max 5 minutes, then fail).

Orphan cleanup on startup: On PP startup, call DescribeInstances with filter tag:pp:managed=true and compare against all non-STOPPED Preview records. Any instance with no matching Preview, or whose matching Preview is STOPPED, is terminated and its security group deleted.

On instance termination:

  1. Delete the AWS key pair (DeleteKeyPair using Preview.sshKeyName).
  2. Delete the security group pp-preview-<previewId>.
  3. Terminate the instance.
  4. Null out Preview.sshPrivateKey and Preview.sshKeyName in the DB.

Deployment Flow

Each PR has exactly one Preview record. The EC2 instance is provisioned once and reused on subsequent pushes.

First push (PR opened / no existing Preview, or previous Preview is STOPPED)

Set Preview status to PROVISIONING.

  1. Check AdminSettings.maxConcurrentInstancesPerUser — count PROVISIONING + BUILDING + RUNNING previews for this user. If at limit: post a Gitea comment on the PR explaining the limit is reached, mark job FAILED, stop.
  2. Provision EC2 — as described above.
  3. SSH in. All subsequent commands run over SSH.
  4. Clone repo:
    • Normal PR: git clone https://<giteaUsername>:<PAT>@<giteaInstanceUrl>/<owner>/<repo>.git /opt/app
    • Fork PR: clone from the fork's URL (payload.pull_request.head.repo.clone_url, injecting PAT auth).
    • cd /opt/app && git fetch origin pull/<prNumber>/head:pp-pr && git checkout pp-pr
  5. Install selected preinstall tools and apt packages: Docker + Compose, Node (using .nvmrc if present, otherwise RepoConfig.nodeVersion), Python, Go, Lua, build tools, and custom RepoConfig.aptPackages.
  6. Write .env: Write /opt/app/.env from RepoConfig.envVars.
  7. Setup commands (if any): run each in order. Only runs on first provision.
  8. Set Preview status to BUILDING.
  9. Build commands: run each in order.
  10. Post-build commands (if any): run each in order.
  11. Start:
    • Docker Compose: cd /opt/app && sudo docker compose -f <composeFilePath> up -d --build --force-recreate
    • Non-compose: run runCommand in background via nohup ... > /opt/app/pp.log 2>&1 &, capture PID → save to Preview.pid.
  12. Set Preview status to RUNNING. Store instanceIp, port, commitSha, lastActivityAt = now.
  13. Comment on PR — post initial status comment (see PR Commenting section). Store the comment ID in Preview.giteaCommentId.

Subsequent push (synchronize / existing RUNNING or FAILED Preview)

  1. SSH into existing instance using Preview.instanceIp and decrypted Preview.sshPrivateKey.
  2. Stop current process:
    • Non-compose: kill <Preview.pid> (SIGTERM, wait 5s, SIGKILL if still running).
    • Docker Compose: cd /opt/app && sudo docker compose -f <composeFilePath> down.
  3. Pull latest:
    cd /opt/app && git fetch origin pull/<prNumber>/head:pp-pr && git checkout pp-pr && git reset --hard FETCH_HEAD
    
  4. Re-run selected preinstall checks (.nvmrc or runtime selections may have changed).
  5. Overwrite .env — re-write from current RepoConfig.envVars.
  6. Set Preview status to BUILDING. Append \n--- Redeploy: <commitSha> ---\n to Preview.logs.
  7. Build commands — re-run each.
  8. Post-build commands — re-run if defined.
  9. Start — same as first provision start step. Update Preview.pid, commitSha, lastActivityAt = now. Set status to RUNNING.
  10. Update PR comment — edit the existing comment in-place using Preview.giteaCommentId (see PR Commenting section).

If any step fails: Set status to FAILED. Append the error output to Preview.logs. Edit the PR comment to show failure with the last few log lines. Stop processing.

All log output is streamed to Preview.logs via WebSocket to the frontend (rj-web WebSocket support). Enforce log size cap: if Preview.logs exceeds AdminSettings.logSizeLimitBytes, drop oldest lines and prepend --- logs truncated ---.

Inactivity Detection

  • Preview.lastActivityAt is updated on each synchronize webhook AND each issue_comment webhook for the PR.
  • A background cron job runs every 30 minutes. Any Preview where lastActivityAt + inactivityHours < now and status = RUNNING gets a STOP job enqueued.
  • On stop: terminate the EC2 instance, post a final PR comment, set status to STOPPED.

Job Queue

Webhook handler returns 200 OK immediately and enqueues a Job. A background worker processes jobs:

  • Per-PR serialisation: at most one active Job per previewId at a time.
  • New deploy cancels running deploy: if a DEPLOY job is already RUNNING for a PR and a new DEPLOY arrives, the running job's SSH session is aborted and the new job takes over immediately.
  • On PP startup: any Jobs left in RUNNING state (from a crash) are reset to PENDING and requeued.
  • Job status (PENDING / RUNNING / DONE / FAILED) is visible on the preview detail page in the UI.

Repo Discovery & Webhook Auto-Registration

  • When the user saves their Gitea credentials, PP fetches all repos via GET <giteaInstanceUrl>/api/v1/repos/search?limit=50&token=<PAT> (paginate as needed).
  • These appear in the Repo Configuration page for the user to enable.
  • When the user enables a repo: PP calls POST <giteaInstanceUrl>/api/v1/repos/{owner}/{repo}/hooks to register a webhook with:
    • type: gitea
    • config.url: https://<PP_BASE_URL>/webhook/<userId>
    • config.secret: WebhookToken.token
    • config.content_type: json
    • events: ["pull_request", "issue_comment"]
    • Store the returned hook ID in RepoConfig.giteaWebhookId.
  • When the user disables or deletes a repo config: PP warns the user that the Gitea webhook will be deleted, and on confirmation calls DELETE <giteaInstanceUrl>/api/v1/repos/{owner}/{repo}/hooks/<giteaWebhookId>.
  • Repos without a RepoConfig that trigger a webhook: PP checks NoConfigComment — if no entry exists for userId + repoOwner + repoName + prNumber, PP posts one comment ("No previews configured for this repo...") and creates a NoConfigComment record.
  • Repo ownership conflict: If a user tries to enable a repo that already has a RepoConfig owned by another user, return an error: "This repo is already configured by another user."

Webhook Secret Rotation

When the user regenerates their webhook secret:

  1. Generate a new WebhookToken.token.
  2. For every RepoConfig belonging to this user that has a giteaWebhookId, call PATCH <giteaInstanceUrl>/api/v1/repos/{owner}/{repo}/hooks/<giteaWebhookId> to update the secret.
  3. Old secret is invalid immediately.

AWS Credential Validation

When the user saves AWS credentials, PP calls sts:GetCallerIdentity. Show inline success ("Connected as arn:aws:iam::...") or error ("Invalid credentials").

Gitea URL Validation

When the user saves their Gitea instance URL, PP calls GET <giteaInstanceUrl>/api/v1/version. Show inline success (Gitea version) or error.

DB Cleanup (background cron, daily)

Delete Preview records where status IN (STOPPED, FAILED) and stoppedAt < now - AdminSettings.previewRetentionDays days. Also delete associated Job records.


Admin Panel (/admin)

Accessible only to isAdmin users. Has its own UI section, separate from the regular user dashboard.

User Management

  • Create users (username + password).
  • Edit username or password of any user.
  • Delete users (cannot delete isFounder user).
  • Promote users to admin (cannot demote isFounder user).
  • Registration endpoint is always disabled — admin creates all accounts.

Global Settings

  • Default EC2 instance type — shown as the default in all repo configs.
  • Max concurrent EC2 instances per user — when a user hits this, new deploys are rejected with a PR comment.
  • Webhook rate limit — max events per user per minute (default 10).
  • Log size cap — max bytes per Preview.logs before truncation (default 1MB).
  • Preview retention — days before STOPPED/FAILED records are purged (default 30).
  • Contact email — shown in the privacy policy footer.

Admin Preview Dashboard

  • Shows all previews across all users (separate from the user's own dashboard).
  • Per preview: user, repo, PR number, status, instance IP, created at.
  • Admin can stop any preview.

User Panel

Account Settings (/settings)

  • Username — editable.
  • Password — change password.
  • Gitea Instance URL — e.g. https://gitea.example.com. Validated on save (GET /api/v1/version).
  • Gitea Username — the account that PP will comment as.
  • Gitea PAT — write-only display (show •••••••• after save). Must have scopes: repository (read), issue (write), admin:repo_hook (for webhook management). Show a tooltip explaining required scopes with a link to Gitea's token settings page.
  • AWS Access Key ID — write-only display after save. Validated on save via sts:GetCallerIdentity.
  • AWS Secret Access Key — write-only display after save.
  • AWS Region — dropdown of all AWS regions.
  • Webhook URL — read-only. https://<PP_BASE_URL>/webhook/<userId>. This is the "Target URL" in Gitea's webhook settings. Copy button included.
  • Webhook Secret — read-only (separate from the URL). This is the "Secret" field in Gitea's webhook settings. Regenerate button (rotates secret and auto-updates all registered hooks in Gitea with a confirmation prompt).

Note displayed in settings: "PP will post PR comments as your Gitea account (@username). Make sure your PAT has 'issue' write permission."

Repo Configuration (/repos)

  • List of repos fetched from Gitea (refresh button). Each repo shows a toggle to enable/disable previews.
  • Enabling a repo auto-registers the webhook in Gitea. Disabling warns and then deletes it.
  • A repo already configured by another PP user shows a lock icon and "Claimed by another user" — the toggle is disabled.
  • Expanded per-repo config:
    • Deny list — tag input of Gitea usernames to skip.
    • EC2 instance type — searchable select (t3.medium, t3.large, t3a.medium, t3a.large, t4g.medium, t4g.large, m5.large, c5.large) + custom text input. Show estimated hourly cost next to each option.
    • Inactivity kill timer — slider 0.5h to 72h with labelled stops.
    • App port — number input (default 3000).
    • Environment variables — key/value editor, values masked. Add/remove rows.
    • Preinstall options — checkboxes for Docker + Compose, Node.js (with version), Python, Go, Lua, build tools, plus custom apt packages.
    • Docker Compose toggle — if on: require Docker, show compose file path input (default docker-compose.yml), hide manual command fields.
    • Commands (shown for both modes unless noted):
      • Apt packages — tag input (compose + non-compose)
      • Setup commands — ordered list, add/remove/reorder (compose + non-compose, run once on first provision)
      • Build commands — ordered list (non-compose only; compose uses docker compose up --build)
      • Post-build commands — ordered list, optional (non-compose only)
      • Run command — single text input (non-compose only)

Preview Dashboard (/ or /previews)

  • Cards for all of this user's previews across repos.
  • Card shows: repo name, PR number + title, status badge, preview URL (clickable when RUNNING), commit SHA, last updated.
  • Click into a preview to see:
    • Full log viewer (WebSocket — live stream while active, monospace, auto-scroll, ANSI codes stripped).
    • Current job status and queue position if PENDING.
    • All previous redeploy separators visible inline in the log.
    • Status history (PROVISIONING → BUILDING → RUNNING etc. with timestamps).

PR Commenting

PP comments using the configured user's Gitea PAT (comments appear as their account). There is no bot account.

One comment per deploy cycle, edited in-place. PP stores the comment ID in Preview.giteaCommentId and calls PATCH /api/v1/repos/{owner}/{repo}/issues/comments/{id} to update it. A new comment is posted fresh at the start of each new push/redeploy cycle (replacing giteaCommentId with the new comment's ID).

Comment format — Markdown:

## 🚀 PR Preview — `<repoOwner>/<repoName>` #<prNumber>

**Status:** 🟡 Provisioning EC2 instance...
**Commit:** `<sha>`
**Updated:** <timestamp>

---
_Powered by [PR Previews](<PP_BASE_URL>)_

Status line updates as the deploy progresses:

  • 🟡 Provisioning EC2 instance...
  • 🟡 Building... (EC2 ready at <ip>)
  • 🟢 Live at http://<ip>:<port>
  • 🔴 Failed — last log lines: (followed by a fenced code block with the last 10 lines of logs)
  • ⚫ Stopped (inactivity timeout / PR closed / manual stop)

On a new redeploy, PP posts a new comment (fresh cycle) and updates giteaCommentId.


Setup Wizard (first login)

Shown to a user on first login if any of the required fields (Gitea URL, PAT, AWS credentials) are unset. Skippable at any point. Steps:

  1. Welcome — brief explanation of what PP does.
  2. Gitea Connection — Gitea instance URL + PAT. Validate on "Next". Show required PAT scopes with a link to Gitea settings.
  3. AWS Setup — Access Key ID, Secret, Region. Show the required IAM policy JSON (copy button). Validate credentials on "Next".
  4. Your Webhook — show the webhook URL and secret. Explain where to find these in Gitea's UI (per-repo or org-level webhook settings). Note: PP auto-registers per-repo webhooks when you enable a repo — this step is just for reference.
  5. Enable your first repo — show the repo list, let the user enable one and fill in its config. On save, PP registers the webhook automatically.
  6. Done — link to the preview dashboard.

UI Design

  • Dark/light mode toggle, persisted to localStorage.
  • Animated route transitions and status badge updates.
  • Status badges: grey (stopped/ignored), 🟡 yellow (provisioning/building), 🟢 green (running), 🔴 red (failed).
  • Log viewer: monospace font, auto-scroll to bottom, ANSI codes stripped, redeploy separator lines visually distinct (dimmed/italic).
  • Responsive layout — usable on mobile for monitoring previews on the go.
  • Toast notifications for save success/failure.
  • Confirmation dialogs for: deleting a user, stopping a preview, regenerating webhook secret, disabling a repo (which deletes the Gitea webhook).
  • Inline validation feedback on settings fields (Gitea URL, AWS credentials, PAT) that tests the connection on save.
  • Contextual tooltips on instance type selector (cost estimates), PAT field (required scopes), inactivity timer (what happens when it fires).

Docker / Deployment

./docker-compose.yml runs PP itself (not preview instances — those are EC2):

  • pp-backend — Node backend.
  • pp-frontend — Vite build served statically (or via backend).
  • pp-db — PostgreSQL.
  • Volume for Prisma migrations.

Required environment variables:

Variable Purpose
DATABASE_URL PostgreSQL connection string
SESSION_SECRET Session signing secret
PP_BASE_URL Public URL of this PP instance (used in webhook URLs and PR comment links)
ENCRYPTION_KEY AES-256 key for encrypting PAT, AWS secrets, SSH private keys at rest

Security Notes

  • Gitea PAT, AWS credentials, and SSH private keys are encrypted at rest with AES-256 using ENCRYPTION_KEY.
  • Webhook HMAC-SHA256 signature is verified before any payload is processed. Invalid signatures return 401.
  • Webhook rate limiting (default 10 req/min/user) prevents floods.
  • EC2 security groups are scoped per preview — created on provision, deleted on termination.
  • SSH keys are ephemeral per launch. Private key is stored encrypted in the DB and nulled out on instance termination.
  • Users can only see and manage their own previews, repos, and credentials.
  • Admins can see all previews in a dedicated admin UI section.
  • The repo config ownership model (first-to-save) prevents two users from deploying conflicting previews for the same repo.

Privacy Policy

A generic self-hosted app privacy policy is included in the footer. It covers:

  • What data PP stores (user credentials, Gitea PAT encrypted at rest, AWS credentials encrypted at rest, preview logs, PR metadata).
  • That credentials are used only to operate the preview service and are never shared with third parties.
  • That EC2 instances are launched in the user's own AWS account — PP does not have access to user data on those instances beyond what it deploys.
  • Log retention policy (previewRetentionDays).
  • How to contact the instance administrator (configurable contact email in admin settings).
  • No analytics, no tracking, no external data sharing.

Out of Scope for V1

  • GitHub/GitLab support (Gitea only).
  • Custom domains for previews (EC2 public IP + port only).
  • Preview instance autoscaling.
  • Billing or usage tracking.
  • Automated tests (manual QA first, tests added in V2).
  • Multi-port previews (one port per repo config only).