1
0
Fork 0
get-installer-bootstrap/docs/superpowers/specs/2026-05-05-get-installer-bootstrap-design.md
2026-05-05 09:11:59 +02:00

8 KiB

Get Installer Bootstrap Design

Goal

Build a small web application that serves cached .sh installer scripts from Git repositories through stable public paths. The primary use case is bootstrapping installation scripts for projects through URLs such as /mcp/graylog/install.sh, while keeping repository details and access tokens managed from an admin interface.

Scope

The first version includes:

  • A public endpoint that serves cached shell scripts for configured paths.
  • An /admin interface with a login screen.
  • Admin account storage in SQLite with hashed passwords.
  • CRUD for script mappings.
  • Optional per-mapping Git access token support for private repositories.
  • Manual synchronization from Git into a local cache.
  • Docker support for production deployment through Coolify.
  • Docker Compose support for local development with the source directory mounted into the PHP container.

The first version does not include:

  • Multi-tenant access control.
  • Scheduled background synchronization.
  • Webhook-triggered synchronization.
  • Multiple active application replicas sharing the same SQLite database.
  • Public directory browsing or repository browsing.

Technology

Use PHP 8.3 with a minimal Symfony application. Symfony provides routing, forms, CSRF protection, password hashing, Doctrine integration, migrations, and test support without needing to reimplement those concerns.

Use SQLite by default through DATABASE_URL. This is enough for one deployed instance and keeps Coolify deployment simple. The application should keep the database configuration compatible with PostgreSQL so that a future deployment can switch to PostgreSQL by changing DATABASE_URL and adding a database service.

Use Nginx in front of PHP-FPM. The production image copies application sources into the image. The development Compose file mounts the local source tree into the container so code changes can be tested without rebuilding.

Main Components

Public Script Serving

The public controller receives all non-admin paths and resolves them against an active mapping. A configured public path such as mcp/graylog/install.sh matches the request path /mcp/graylog/install.sh.

The public endpoint never contacts Git directly. It serves only the current cached file for the mapping. If no active mapping exists, or if the mapping has never synchronized successfully, the endpoint returns 404.

Served files should use a shell-friendly content type such as text/x-shellscript; charset=UTF-8, with Content-Disposition: inline.

Admin Interface

/admin displays a login screen when the user is not authenticated. Admin users are stored in the application database and passwords are hashed with Symfony's password hasher.

After login, the admin can:

  • List mappings.
  • Create a mapping.
  • Edit a mapping.
  • Delete a mapping.
  • Trigger synchronization for one mapping.
  • See the last synchronization status, last synchronization timestamp, and last synchronization error.

All admin forms use CSRF protection.

Mapping Model

Each mapping stores:

  • Public path, for example mcp/graylog/install.sh.
  • Git repository URL, for example https://forge.lclr.dev/AI/graylog-mcp.git.
  • Git ref, for example main.
  • File path inside the repository, for example install.sh.
  • Optional access token.
  • Active flag.
  • Last synchronization status.
  • Last successful synchronization timestamp.
  • Last synchronization error.
  • Cached file path or cache key.

Public paths and repository file paths are normalized before storage and before use. They must be relative paths, must not contain .., and public paths must end in .sh.

Git Synchronization

Synchronization is manual from the admin interface.

For each mapping, the synchronizer:

  1. Creates or reuses a per-mapping working directory under a cache volume.
  2. Clones the repository when the local working directory does not exist.
  3. Fetches updates when it already exists.
  4. Checks out the configured ref.
  5. Validates that the configured repository file path resolves inside the checkout.
  6. Validates that the target is a regular file.
  7. Copies the file into a stable served cache location for the mapping.
  8. Updates synchronization metadata in the database.

If synchronization fails, the previous served cache remains in place. The mapping records the error so the admin can diagnose the issue without breaking already working installer URLs.

For private HTTPS repositories, the synchronizer uses the configured access token only during Git operations. The token is never exposed in public responses or admin list views. Forms may allow replacing or clearing a token without displaying the stored value.

Storage Layout

The application uses two persistent locations:

  • Database volume: stores SQLite database files.
  • Cache volume: stores Git working directories and served script copies.

The application never serves files directly from arbitrary disk paths. Public responses always go through the mapping lookup and cache resolver.

Docker And Deployment

Production deployment uses Docker Compose with at least:

  • An app/PHP-FPM container built from the repository.
  • An Nginx container using a checked-in Nginx config.
  • Volumes for the SQLite database and Git/script cache.

The production Dockerfile copies source files into the image and installs PHP dependencies during the image build.

Development uses a Compose override or a dedicated development Compose file that mounts the local repository into the app container. This allows editing Symfony files locally and testing immediately without rebuilding the image.

Security

Admin authentication is required for every /admin route except login.

Tokens are sensitive data. They must not be logged, displayed in list pages, or returned by public endpoints. Error messages should identify which mapping failed and why, without printing credentials or credential-bearing Git URLs.

Path handling is strict:

  • Public paths are relative.
  • Public paths cannot contain empty segments, . segments, or .. segments.
  • Public paths must end with .sh.
  • Repository file paths are relative.
  • Repository file paths cannot escape the checkout directory.
  • Public serving cannot fall back to arbitrary files on disk.

The admin interface uses CSRF protection for state-changing actions.

Error Handling

Public endpoint behavior:

  • Unknown path: 404.
  • Known mapping without successful cache: 404.
  • Inactive mapping: 404.
  • Unexpected read failure: 500, with details in server logs only.

Admin synchronization behavior:

  • Git failures are stored on the mapping as the last synchronization error.
  • The previous cached script remains available when present.
  • The admin screen shows success or failure after synchronization.

Production logs should identify the mapping, public path, repository host/path when useful, and the operation that failed. Logs must not include access tokens.

Testing

The implementation should include focused tests for:

  • Public path normalization and rejection of dangerous paths.
  • Repository file path normalization and checkout escape prevention.
  • Public endpoint returns cached script content for a valid synchronized mapping.
  • Public endpoint returns 404 for unknown, inactive, or unsynchronized mappings.
  • Admin login protects /admin.
  • Mapping creation validates required fields and .sh public paths.
  • Synchronization preserves the previous cached file when Git update fails.

Git synchronization can be tested against local fixture repositories to avoid network dependency.

Regression Risk

The highest risk areas are path handling, token handling, and cache update atomicity. The implementation should keep these responsibilities isolated in small services so the public controller, admin forms, and synchronizer do not duplicate security-sensitive logic.

SQLite is acceptable for the first deployment because the app is intended to run as a single instance. If the deployment later needs multiple active replicas, PostgreSQL should replace SQLite before scaling horizontally.