Skip to content
Open
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
36 changes: 36 additions & 0 deletions dev-server-provision/coder/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ variable "docker_socket" {
type = string
}

variable "starter_template" {
default = "none"
description = "Starter app template to bootstrap in new workspaces. Options: none, fullstack-baseline"
type = string
}

provider "docker" {
host = var.docker_socket != "" ? var.docker_socket : null
}
Expand Down Expand Up @@ -55,6 +61,35 @@ resource "coder_agent" "main" {
else
echo "[remotevibe] WARNING: $AGENT_ENV not found — AI agents may not be authenticated"
fi

# ── Bootstrap starter app template ──────────────────────────────────────
STARTER_TEMPLATE="$${STARTER_TEMPLATE:-none}"
if [ "$STARTER_TEMPLATE" = "fullstack-baseline" ]; then
TMPL_DIR="/home/coder/fullstack-baseline"
if [ ! -f "$TMPL_DIR/.bootstrapped" ]; then
echo "[remotevibe] Bootstrapping fullstack-baseline (tiangolo/full-stack-fastapi-template)…"
git clone --depth=1 https://github.com/tiangolo/full-stack-fastapi-template "$TMPL_DIR" 2>&1 | sed 's/^/[remotevibe] /'
cd "$TMPL_DIR"
Comment on lines +71 to +72
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent clone failures from aborting workspace startup

The starter-template block is in the main startup_script, so with set -e a failed git clone can stop the entire agent startup before code-server is launched. In the common failure path where GitHub is temporarily unreachable (or clone fails before the target dir exists), the next cd "$TMPL_DIR" exits non-zero and the workspace fails to come up even though this feature is optional. Wrapping clone/bootstrap in a non-fatal guard (log + continue) would avoid taking down the whole workspace.

Useful? React with 👍 / 👎.


# Generate .env from the example — patch project name and secret key
cp .env "$TMPL_DIR/.env.bak" 2>/dev/null || true
sed \
-e "s|^PROJECT_NAME=.*|PROJECT_NAME=fullstack-baseline|" \
-e "s|^SECRET_KEY=.*|SECRET_KEY=$(openssl rand -hex 32)|" \
-e "s|^FIRST_SUPERUSER=.*|FIRST_SUPERUSER=admin@example.com|" \
-e "s|^FIRST_SUPERUSER_PASSWORD=.*|FIRST_SUPERUSER_PASSWORD=changeme123|" \
.env > .env.patched && mv .env.patched .env

docker compose up -d 2>&1 | sed 's/^/[remotevibe] /'
touch "$TMPL_DIR/.bootstrapped"
Comment on lines +83 to +84
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Check compose result before creating bootstrap marker

docker compose up -d is piped through sed, but the script does not enable pipefail, so this pipeline is treated as successful when sed succeeds even if Compose fails. The code then immediately writes .bootstrapped, which suppresses full bootstrap on later starts and can leave users with a marked-as-complete template that never actually started. Capture and validate the Compose exit status (or enable set -o pipefail) before touching the marker.

Useful? React with 👍 / 👎.

echo "[remotevibe] fullstack-baseline stack started."
echo "[remotevibe] Frontend : http://localhost:5173"
echo "[remotevibe] API docs : http://localhost:8000/docs"
Comment on lines +71 to +87
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This starter-template bootstrap is optional, but set -e plus unguarded git clone, .env patching, and the first docker compose up -d means any transient failure will abort the entire agent startup and can leave the workspace in a failed state. Wrap the bootstrap block with error handling (log and continue), and consider writing .bootstrapped only after all required steps succeed.

Suggested change
git clone --depth=1 https://github.com/tiangolo/full-stack-fastapi-template "$TMPL_DIR" 2>&1 | sed 's/^/[remotevibe] /'
cd "$TMPL_DIR"
# Generate .env from the example — patch project name and secret key
cp .env "$TMPL_DIR/.env.bak" 2>/dev/null || true
sed \
-e "s|^PROJECT_NAME=.*|PROJECT_NAME=fullstack-baseline|" \
-e "s|^SECRET_KEY=.*|SECRET_KEY=$(openssl rand -hex 32)|" \
-e "s|^FIRST_SUPERUSER=.*|FIRST_SUPERUSER=admin@example.com|" \
-e "s|^FIRST_SUPERUSER_PASSWORD=.*|FIRST_SUPERUSER_PASSWORD=changeme123|" \
.env > .env.patched && mv .env.patched .env
docker compose up -d 2>&1 | sed 's/^/[remotevibe] /'
touch "$TMPL_DIR/.bootstrapped"
echo "[remotevibe] fullstack-baseline stack started."
echo "[remotevibe] Frontend : http://localhost:5173"
echo "[remotevibe] API docs : http://localhost:8000/docs"
if git clone --depth=1 https://github.com/tiangolo/full-stack-fastapi-template "$TMPL_DIR" 2>&1 | sed 's/^/[remotevibe] /' \
&& cd "$TMPL_DIR" \
&& cp .env "$TMPL_DIR/.env.bak" 2>/dev/null || true \
&& sed \
-e "s|^PROJECT_NAME=.*|PROJECT_NAME=fullstack-baseline|" \
-e "s|^SECRET_KEY=.*|SECRET_KEY=$(openssl rand -hex 32)|" \
-e "s|^FIRST_SUPERUSER=.*|FIRST_SUPERUSER=admin@example.com|" \
-e "s|^FIRST_SUPERUSER_PASSWORD=.*|FIRST_SUPERUSER_PASSWORD=changeme123|" \
.env > .env.patched \
&& mv .env.patched .env \
&& docker compose up -d 2>&1 | sed 's/^/[remotevibe] /'; then
touch "$TMPL_DIR/.bootstrapped"
echo "[remotevibe] fullstack-baseline stack started."
echo "[remotevibe] Frontend : http://localhost:5173"
echo "[remotevibe] API docs : http://localhost:8000/docs"
else
echo "[remotevibe] WARNING: fullstack-baseline bootstrap failed; continuing workspace startup without starter stack."
fi

Copilot uses AI. Check for mistakes.
else
echo "[remotevibe] fullstack-baseline already bootstrapped — ensuring stack is up…"
docker compose -f "$TMPL_DIR/docker-compose.yml" up -d 2>&1 | sed 's/^/[remotevibe] /' || true
fi
Comment on lines +65 to +91
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The startup script runs docker compose up -d inside the workspace container, but this template does not mount a Docker socket and the workspace image only installs the Docker CLI (no daemon). As a result, docker compose will fail to connect and the starter template bootstrap won’t work. Consider either mounting /var/run/docker.sock (and documenting the security tradeoff) or implementing the stack without requiring Docker-in-workspace (e.g., run services as additional Terraform-managed containers).

Copilot uses AI. Check for mistakes.
fi
# ── Configure Coder CLI for workspace template management ────────────────
if [ -f /run/secrets/coder-token ]; then
_tok=$(cat /run/secrets/coder-token | tr -d '[:space:]')
Expand Down Expand Up @@ -200,6 +235,7 @@ resource "docker_container" "workspace" {
"CODER_URL=${data.coder_workspace.me.access_url}",
"CODER_TEMPLATE_NAME=remotevibe",
"CODER_TEMPLATE_DIR=/workspace/template",
"STARTER_TEMPLATE=${var.starter_template}",
]

host {
Expand Down
49 changes: 49 additions & 0 deletions dev-server-provision/configurator/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,52 @@ def _ask_agents(config: dict[str, Any]) -> None:
_success("Reusing Google API key already provided.")


# ---------------------------------------------------------------------------
# Step 4b — Starter app template
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The section header says “Step 4b — Starter app template”, but run() invokes _ask_starter_template() after the AI agents step (labeled “5b” below). Update the step numbering/comment to avoid confusion when maintaining the wizard flow.

Suggested change
# Step 4b — Starter app template
# Step 5b — Starter app template

Copilot uses AI. Check for mistakes.
# ---------------------------------------------------------------------------

_STARTER_TEMPLATE_CHOICES = [
{
"name": "none — plain workspace (no pre-installed app)",
"value": "none",
},
{
"name": "fullstack-baseline — FastAPI + PostgreSQL + React/Vite (tiangolo/full-stack-fastapi-template)",
"value": "fullstack-baseline",
},
]


def _ask_starter_template(config: dict[str, Any]) -> None:
_heading("Starter App Template (optional)")
print(
" Optionally pre-install a starter application into every new workspace.\n"
f"\n"
f" {_BOLD}fullstack-baseline{_RESET} bootstraps "
f"{_CYAN}tiangolo/full-stack-fastapi-template{_RESET}:\n"
f" • FastAPI backend (Python, SQLModel, Alembic migrations)\n"
f" • PostgreSQL database\n"
f" • React + Vite frontend\n"
f" • JWT auth + RBAC + CRUD scaffolding\n"
f" • Docker Compose stack (ready in ~2 min)\n"
f"\n"
f" The stack starts automatically on first workspace launch and is\n"
f" available at http://localhost:5173 (frontend) and :8000 (API).\n"
)

current = config.get("starter_template") or "none"
config["starter_template"] = inquirer.select(
message="Starter template:",
choices=_STARTER_TEMPLATE_CHOICES,
default=current,
).execute()

if config["starter_template"] == "fullstack-baseline":
_success("fullstack-baseline selected — stack will auto-start on first workspace launch.")
else:
_success("No starter template — plain workspace.")


# ---------------------------------------------------------------------------
# Step 4 — Coder admin password
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -782,6 +828,9 @@ def run() -> None:
# 5. AI agents
_ask_agents(config)

# 5b. Starter app template
_ask_starter_template(config)

# 6. Provider options
_ask_provider_options(provider, deploy_config)

Expand Down
8 changes: 8 additions & 0 deletions dev-server-provision/configurator/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@
# ── OpenCode Provider Selection ────────────────────────────────────
OPENCODE_PROVIDER={opencode_provider}

# ── Starter App Template ───────────────────────────────────────────
# none — no template (plain workspace)
# fullstack-baseline — tiangolo/full-stack-fastapi-template
# (FastAPI + PostgreSQL + React/Vite + Auth + RBAC)
STARTER_TEMPLATE={starter_template}

- path: /etc/dev-server/bootstrap.sh
permissions: "0700"
owner: root:root
Expand Down Expand Up @@ -192,6 +198,7 @@ def default_config() -> dict[str, Any]:
"github_token": "",
"codex_openai_auth_code": "",
"opencode_provider": "",
"starter_template": "none",
}


Expand Down Expand Up @@ -219,6 +226,7 @@ def default_config() -> dict[str, Any]:
"github_token",
"codex_openai_auth_code",
"opencode_provider",
"starter_template",
]


Expand Down
27 changes: 27 additions & 0 deletions dev-server-provision/configurator/tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
validate_domain,
validate_email,
validate_oauth_client_id,
validate_starter_template,
validate_subdomain,
)

Expand Down Expand Up @@ -155,6 +156,7 @@ def _full_config(self, **overrides):
"email": "admin@example.com",
"cloudflare_api_token": "a" * 40,
"cloudflare_zone_id": "a" * 32,
"coder_admin_password": "TestPass123",
"enable_agent_copilot": False,
"enable_agent_claude": False,
"enable_agent_gemini": False,
Expand Down Expand Up @@ -344,6 +346,31 @@ def test_opencode_unknown_provider_rejected(self):
self.assertIn("not supported", agent_check.message)


class TestValidateStarterTemplate(unittest.TestCase):
def test_none_accepted(self):
self.assertTrue(validate_starter_template("none"))

def test_fullstack_baseline_accepted(self):
self.assertTrue(validate_starter_template("fullstack-baseline"))

def test_case_insensitive(self):
self.assertTrue(validate_starter_template("Fullstack-Baseline"))
self.assertTrue(validate_starter_template("NONE"))

def test_unknown_rejected(self):
result = validate_starter_template("my-custom-template")
self.assertIsInstance(result, str)
self.assertIn("my-custom-template", result)

def test_empty_rejected(self):
result = validate_starter_template("")
self.assertIsInstance(result, str)

def test_whitespace_stripped(self):
self.assertTrue(validate_starter_template(" none "))
self.assertTrue(validate_starter_template(" fullstack-baseline "))


class TestPreflightResult(unittest.TestCase):
def test_repr_pass(self):
r = PreflightResult("test", True, "OK")
Expand Down
20 changes: 19 additions & 1 deletion dev-server-provision/configurator/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def validate_coder_password(value: str) -> str | bool:
return True



def validate_api_key_optional(value: str) -> str | bool:
"""Accept empty or any non-whitespace string."""
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_api_key_optional() currently returns True for all inputs, including whitespace-only strings, which contradicts its docstring (“non-whitespace string”). Either implement the documented check (treat whitespace-only as invalid or as empty) or update the docstring to match the intended behavior.

Suggested change
"""Accept empty or any non-whitespace string."""
"""Accept empty or any non-whitespace string."""
value = value.strip()
if not value:
return True

Copilot uses AI. Check for mistakes.
return True

Expand Down Expand Up @@ -113,6 +113,24 @@ def validate_callback_url_or_code(value: str) -> str | bool:
return True


_VALID_STARTER_TEMPLATES = ("none", "fullstack-baseline")


def validate_starter_template(value: str) -> str | bool:
"""Validate the starter template selection.

Accepted values: ``"none"`` (no template) or ``"fullstack-baseline"``
(tiangolo/full-stack-fastapi-template — FastAPI + PostgreSQL + React).
"""
value = value.strip().lower()
if value not in _VALID_STARTER_TEMPLATES:
return (
f"Unknown starter template '{value}'. "
f"Valid options: {', '.join(_VALID_STARTER_TEMPLATES)}."
)
return True


# ---------------------------------------------------------------------------
# Preflight checks
# ---------------------------------------------------------------------------
Expand Down
53 changes: 53 additions & 0 deletions dev-server-provision/docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,59 @@ which codex # if ENABLE_AGENT_CODEX=true
which opencode # if ENABLE_AGENT_OPENCODE=true
```

## Step 7: (Optional) Starter App Templates

RemoteVibeServer can automatically bootstrap a production-ready starter
application inside every new Coder workspace.

### Available templates

| Value | Description |
|---------------------|------------------------------------------------------------|
| `none` | No template — plain workspace (default) |
| `fullstack-baseline`| FastAPI + PostgreSQL + React/Vite + Auth + RBAC (tiangolo) |

### Enabling a template

Set `STARTER_TEMPLATE` in `/etc/dev-server/env` **before** provisioning:

```yaml
# In RVSconfig.yml / cloud-init.yaml:
STARTER_TEMPLATE: "fullstack-baseline"
```
Comment on lines +221 to +226
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section says to set STARTER_TEMPLATE in /etc/dev-server/env before provisioning, but the example shown is YAML for RVSconfig.yml / cloud-init.yaml. Consider rewording to: set it in your cloud-init.yaml/RVSconfig.yml (which is rendered into /etc/dev-server/env during provisioning) to avoid implying manual edits to /etc/dev-server/env pre-provision.

Copilot uses AI. Check for mistakes.

Or via the configurator CLI — a new step `Starter App Template` appears after
the AI Agents step.

### What fullstack-baseline provides

Based on **[tiangolo/full-stack-fastapi-template](https://github.com/tiangolo/full-stack-fastapi-template)**:

- **FastAPI** backend (Python, SQLModel, Alembic migrations)
- **PostgreSQL 16** database
- **React + Vite** frontend (TypeScript, Chakra UI)
- JWT auth + RBAC (superuser / regular-user roles)
- Docker Compose stack — starts automatically on workspace launch

**Default ports inside the workspace:**

| Service | URL |
|------------------|-----------------------------------|
| Frontend | http://localhost:5173 |
| Backend API | http://localhost:8000 |
| Interactive docs | http://localhost:8000/docs |

**Default credentials:** `admin@example.com` / `changeme123` (change on first login)

### Template lifecycle

- The stack is cloned and started on the **first workspace launch** (~2 min)
- Subsequent starts run `docker compose up -d` (fast — containers already exist)
- A `.bootstrapped` marker prevents re-cloning; local edits persist across restarts
- Remove `.bootstrapped` and restart the workspace to reset to a clean clone

For full documentation see [`templates/fullstack-baseline/README.md`](../../templates/fullstack-baseline/README.md).

## Troubleshooting

### Provisioning failed
Expand Down
3 changes: 2 additions & 1 deletion dev-server-provision/infra/agents.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ log "Writing agent API key file → $AGENT_ENV_FILE …"
for key in GITHUB_TOKEN OPENAI_API_KEY ANTHROPIC_API_KEY GOOGLE_API_KEY \
CODEX_OPENAI_AUTH_CODE OPENCODE_PROVIDER \
ENABLE_AGENT_COPILOT ENABLE_AGENT_CLAUDE ENABLE_AGENT_GEMINI \
ENABLE_AGENT_CODEX ENABLE_AGENT_OPENCODE; do
ENABLE_AGENT_CODEX ENABLE_AGENT_OPENCODE \
STARTER_TEMPLATE; do
value="$(grep "^${key}=" "$ENV_FILE" | cut -d= -f2- || true)"
echo "${key}=${value}"
done
Expand Down
85 changes: 85 additions & 0 deletions templates/fullstack-baseline/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# fullstack-baseline — Starter App Template

A production-ready fullstack application that is automatically bootstrapped
inside every new Coder workspace when `STARTER_TEMPLATE=fullstack-baseline` is
configured.

It is based on **[tiangolo/full-stack-fastapi-template](https://github.com/tiangolo/full-stack-fastapi-template)**
— the official FastAPI project template maintained by the FastAPI author.

---

## What you get

| Layer | Technology |
|-------------|-----------------------------------------------|
| Backend API | FastAPI (Python) + SQLModel ORM |
| Database | PostgreSQL 16 |
| Migrations | Alembic |
| Frontend | React + Vite + TypeScript + Chakra UI |
| Auth | JWT bearer tokens (login / refresh) |
| RBAC | Superuser + regular-user roles |
| CRUD | Items resource (scaffold for your own models) |
| Dev stack | Docker Compose |

---

## Ports (inside the workspace)

| Service | URL |
|------------------|-----------------------------------|
| Frontend | http://localhost:5173 |
| Backend API | http://localhost:8000 |
| Interactive docs | http://localhost:8000/docs |
| Adminer (DB UI) | http://localhost:8080 |

---

## Default credentials

| Field | Value |
|----------|------------------------|
| Email | `admin@example.com` |
| Password | `changeme123` |

Change the password immediately after first login.

---

## How it works

When a workspace starts with `STARTER_TEMPLATE=fullstack-baseline`, the
`startup_script` in `coder/main.tf`:

1. Clones `tiangolo/full-stack-fastapi-template` into `~/fullstack-baseline`
2. Patches `.env` (project name, random `SECRET_KEY`, default superuser)
3. Runs `docker compose up -d`
4. Creates `~/fullstack-baseline/.bootstrapped` to make subsequent starts
idempotent

The stack is started on every workspace launch (via `docker compose up -d`),
and stopped automatically when the workspace is stopped.

---

## Re-running the bootstrap

If the bootstrap was interrupted or you want a clean slate:

```bash
rm ~/fullstack-baseline/.bootstrapped
# restart the workspace, or run the startup script manually
```

## Customising

Fork the template or edit files directly in `~/fullstack-baseline`. The
`.bootstrapped` marker prevents re-cloning, so local changes persist across
workspace restarts.

For a clean re-clone:

```bash
rm -rf ~/fullstack-baseline
# restart the workspace
```