# fern-platform is a test analytics platform whose documented Docker install is still marked coming soon at version v1.0.1

> A Go and React platform that ingests results from any test framework over REST, stores them in Postgres behind a GraphQL API, and ranks suite health in a treemap. The supported install path is a k3d cluster with Keycloak rather than a container, and two frontends ship side by side.

**guidewire-oss/fern-platform** — Unified test intelligence platform with multi-format ingestion, real-time analytics, and AI-powered insights via LLM integration

- Repository: https://github.com/guidewire-oss/fern-platform
- Stars: 459 · Forks: 32
- Language: Go
- License: Apache-2.0
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/guidewire-oss-fern-platform

## The Docker install is documented as arriving after a release that already shipped twice

Option 1 in the installation section is headed Docker, Coming Soon, and the text beneath it says Docker images will be available after the v0.1.0 release. The two image references it lists are GitHub Container Registry at `ghcr.io/guidewire-oss/fern-platform:latest` and Docker Hub at `docker.io/guidewireoss/fern-platform:latest`. The `docker run` command underneath is labelled as future usage that is not yet available, and the instruction that follows is to use the Kubernetes option or build from source instead.

The awkward part is the version arithmetic. The repository's tagged releases are v1.0.0 in July 2026 and v1.0.1 in September 2026, both of which postdate the v0.1.0 the note is waiting on, and there is a v0.2.0 before them. So the gate on container publishing, as written, has already been passed twice while the documentation still presents the container path as unavailable. Whether the images exist and the note has gone stale, or publishing genuinely never happened, is exactly the thing to check before planning around `ghcr.io/guidewire-oss/fern-platform:latest`.

There is a second Docker story that is fully built even if the image is not, which is worth separating. A local test stack exists for sanity-checking changes without standing up a cluster, and it is wired to `make docker-test-up`, `make docker-test-down`, and `make docker-test-curl`, the last being a quick smoke probe against `/healthz`, `/readyz`, and `/api/v1/health`. So development against containers works; distribution as a container is the part in question.

## The supported path is a k3d cluster with Keycloak and 8GB of RAM

Option 2 is labelled Kubernetes with OAuth, Full Features, and it is the path the documentation actually tells you to use. It runs against k3d, the lightweight Kubernetes distribution, and it starts by editing your hosts file, because OAuth needs a resolvable hostname rather than a bare port:

```bash
# Clone the repository
git clone https://github.com/guidewire-oss/fern-platform
cd fern-platform

# Add required hosts entries (for OAuth to work)
echo "127.0.0.1 fern-platform.local" | sudo tee -a /etc/hosts
echo "127.0.0.1 keycloak" | sudo tee -a /etc/hosts

# Deploy with the v2 SPA frontend (recommended)
make deploy-all-v2

# Or deploy with the classic v1 frontend
make deploy-all
```

Two deploy targets exist and the recommendation is explicit: `deploy-all-v2` for the React SPA, `deploy-all` for the classic interface. They land on the same port at different paths, with the v2 app at `/v2` and v1 at the root.

The prerequisites are heavier than a typical local tool. Alongside k3d and kubectl you need Docker with buildx, Go 1.21 or newer used by the Makefile for architecture detection, Make, and a stated minimum of 8GB RAM. Keycloak is deployed as part of the stack, which is why the hosts entry for it exists.

One operational wrinkle is documented for corporate networks. If TLS inspection stops your cluster nodes pulling from Docker Hub, there is a `make deploy-quick-v2` target to use after manually importing the required images, specifically `redis:7-alpine` and `quay.io/keycloak/keycloak:23.0`. That is a real deployment, not a container you run, and the published login is `admin@fern.com` with the password `test123`.

## The local stack turns auth off so you can exercise ingestion without Keycloak

The compose file is explicitly a local test stack for sanity-checking changes, and its header states the intent: bring up Postgres, Redis, and the platform binary built from the current checkout, intended for avoiding the full k3d cluster. The auth decision is stated rather than implied. Auth is disabled by default so the v1 ingestion and v2 list endpoints can be exercised without a Keycloak realm, and to exercise OAuth you are pointed at the k3d deployment instead.

That split has a consequence worth internalising. Anything you verify against the compose stack has been verified with authentication switched off, so a green local run says nothing about the authorization path that production will use. It also means the compose stack is the right place to test ingestion throughput and the wrong place to test access control.

Two implementation details are worth noting because they are the kind of thing that saves an afternoon. Postgres is `postgres:14-alpine`, published on host port 55432 rather than 5432, which avoids colliding with a Postgres you may already be running locally. And there is a one-shot init service that creates a role named `app` with `NOLOGIN`, described as a k3d and CloudNativePG convention baked into a handful of legacy migrations. That service runs after Postgres is healthy and before the platform starts, and it tolerates the role already existing, swallowing the `already exists` message and treating the 49P04 duplicate-object code as fine, so restarting against the same volume does not fail. Small tolerances like that are usually the difference between a stack you can rebuild casually and one you rebuild by hand.

## Two frontends ship at once, and the v2 SPA is embedded into the Go binary

The repository carries both a `web/` and a `web-v2/` directory, and the v2 app is opt-in rather than replacing v1. It is served at `/v2` and brings filtering, saved views, and treemap drill-down, while the classic v1 interface remains at the root. Anyone with existing bookmarks or bookmarks shared in a wiki keeps working.

The build path explains how a React app ends up inside a Go binary. The Dockerfile's first stage is Node 20 Alpine, and its comment says the stage exists so contributors without local Node still get a working image. The stated preference is the opposite way round: the `web-v2-build` make target does the build on the host when pnpm is available, and the container stage is the fallback. Either way the output lands in `internal/web/dist`, and a Go embed directive in `internal/web/embed.go` captures whatever is in that directory at Go-build time.

The ordering in the Dockerfile is what makes this reliable. The freshly built SPA is copied into `internal/web/dist` before the `go build` runs, with a comment noting it is done specifically so the embed picks up the latest bundle even if the host never ran the host-side build. Install the frontend with a frozen pnpm lockfile via corepack, then build, then compile.

The Go stage is Go 1.24.5 Alpine with cross-compilation arguments for target OS and architecture, and it compiles two binaries rather than one. The main binary comes from `cmd/fern-platform/main.go`, and a second comes from `cmd/seed`, described as the load-test seeder. Carrying both in one image is a deliberate convenience so a seeding run works with a compose run of the same service instead of a second build.

## One dependency list holds two ORMs, a GraphQL codegen stack, and a dataloader

The Go module targets version 1.24 and the direct dependencies describe the architecture more clearly than the prose does. The HTTP layer is gin with CORS, logging is logrus, and configuration is viper. Authentication uses JWT version 5, and there is a gorilla websocket dependency for live updates.

The persistence story has two halves. GORM is present with both `gorm.io/driver/postgres` and `gorm.io/driver/sqlite`, so SQLite is a supported target alongside Postgres rather than an afterthought. The Postgres path also carries pgx version 5, and migrations are handled by golang-migrate with a `migrations/` directory in the tree.

Querying is the most distinctive part. The GraphQL stack is not one library but four: gqlgen for schema-first code generation, a gqlgen config file in the repository root, gqlparser for parsing, and `graph-gophers/dataloader/v7` for batching. The dataloader is the one to notice, because a naive GraphQL resolver over test-run rows issues a query per parent node, and batching is what keeps a treemap drill-down from turning into thousands of round trips. The dependency also implies pagination is cursor-style rather than offset-style, since dataloaders key on row identifiers.

Testing leans on Ginkgo and Gomega as the pair, with testify and `go-sqlmock` alongside for ordinary unit tests. That combination suggests both a behavioural suite and conventional mocks, which is a normal pairing for a service that has to be testable without Postgres in the loop.

## Six included Makefiles are scanned by awk into a single help screen

The build system is one Makefile that includes six others: Makefile.core, Makefile.test, Makefile.docker, Makefile.k8s, Makefile.ci, and Makefile.web. The main file then sets a default goal of `help` and declares its own phony targets for help, all, quick-start, and teardown.

The help implementation is the interesting part. Rather than maintaining a hand-written target list that drifts, the recipe runs awk over each included file and pulls out targets carrying a double-hash comment, then prints them grouped by section. There are separate passes for core development, testing, Docker, Kubernetes, and CI/CD, each filtering the same `## ` convention with a different regular expression and a different colour. The default goal printing the list, grouped and coloured, without a central registry to update, is a clean way to keep six files honest.

The rest of the target vocabulary follows from the earlier sections. `setup-local` and `dev` are the development entry points, `deploy-all` and `deploy-all-v2` are the two k3d deployments, `deploy-quick-v2` is the proxy-workaround path, and the `docker-test-*` family covers the local compose stack. One more thing sits in the root of the repository that hints at the project's current focus: `perf-budgets.json`, alongside two documents about performance work.

## Client libraries are separate repositories, and writing your own is a documented path

Because the ingestion surface is a REST API that accepts results from any framework, the client is a thin reporter rather than a dependency of your test suite. Three official ones exist, each in its own repository: a Go and Ginkgo client, a Java and JUnit client with a companion Gradle plugin, and a JavaScript and Jest client.

Each attaches differently depending on the framework. For Jest it is a reporter entry in the config, taking the platform URL and project id from the environment. For Gradle it is a plugin applied by id and version, with a block supplying the same two values. For Ginkgo it is a Go reporter constructed in a suite setup hook. The project documentation also points at a client development guide for building clients for Python, Ruby, PHP, .NET, or anything else, and names pytest, RSpec, PHPUnit, and NUnit as frameworks to wire up.

That design keeps the platform out of your build's critical path in a way a plugin would not. If the platform is down, the reporter fails to deliver and your tests still run, which is the right failure mode for a test suite. It also means adding a framework is independent work that does not require changing the server.

The two environment variables are the entire client configuration:

```bash
export FERN_PROJECT_ID=my-project
export FERN_URL=http://fern-platform.local:8080
```

Once those are set, results are sent automatically, with no further registration step beyond a manager having created the project in the interface first.

## GraphQL is the analysis surface, and JIRA linkage has its own generated key

Ingestion is REST, but analysis is GraphQL, and the documented entry point is a query that takes a project id and a page size:

```graphql
query {
  testRuns(projectId: "my-project", first: 10) {
    runs {
      id
      status
      duration
      gitCommit
    }
  }
}
```

The shape of that query tells you what the model considers first-class. A run has a status, a duration, and the git commit it came from, which is what makes historical debugging possible: you can ask which commit turned a test red. The `first` argument is the cursor-style pagination that pairs with the dataloader in the dependency list.

JIRA linkage is the one feature that needs a secret before the platform starts. The container run block generates one with `JIRA_KEY=$(openssl rand -hex 32)`, marked optional and only needed for that integration, and passes it as `JIRA_ENCRYPTION_KEY`. Generating a 32-byte hex key rather than a passphrase is the right shape for an encryption key, and the fact that it is created at deploy time rather than shipped is the correct habit.

The documentation is organised by audience rather than by feature, which is a decent signal of who the project expects to use it. Users get a UI features guide, workflows, and use cases. Developers get the integration guide, linking tests to JIRA, an API reference, and GraphQL docs. DevOps get installation, configuration, and troubleshooting. Contributors get architecture, the contributing guide, and a directory of RFCs. A `mock-jira/` directory in the tree suggests the JIRA integration is tested against a stand-in rather than a live instance.

## Conclusion

fern-platform fits a team whose test failures are expensive to diagnose because the evidence is scattered across CI providers and frameworks, and who want one treemap of suite health plus a GraphQL surface to ask harder questions. It does not fit anyone who wants a single container today, since the Docker path is still described as arriving after a release that has already happened twice over. Verify three things before planning a rollout. That the image tags named in the documentation actually resolve, since the coming-soon note and the v1.0.1 tag contradict each other. That Keycloak is acceptable in your environment, because OAuth is on the supported path and the local stack deliberately turns auth off. And that you change the documented default login before anyone else reaches the deployment, since `admin@fern.com` and `test123` are published in the readme and the k3d path binds a hostname through your hosts file.

## FAQ

### What is Fern Platform?

It is a unified test intelligence platform that aggregates test results from any CI/CD pipeline or testing framework, including Jest, pytest, and JUnit, into a centralized dashboard. It detects flaky tests, tracks test execution times, and renders suite health as a treemap.

### How do I install Fern Platform?

The documented full-feature path is k3d: clone the repository, add hosts entries for fern-platform.local and keycloak, then run `make deploy-all-v2` for the v2 SPA or `make deploy-all` for the classic frontend. The Docker option is marked Coming Soon, and the docs direct you to Kubernetes or building from source instead.

### What are the default Fern Platform credentials?

The documentation publishes `admin@fern.com` with the password `test123`. The k3d path depends on a hostname mapped in your hosts file, which is required for OAuth to work with the bundled Keycloak.

### Can I run Fern Platform without OAuth?

Yes. The local compose test stack disables auth by default so the v1 ingestion and v2 list endpoints can be exercised without a Keycloak realm. To exercise OAuth you need the k3d deployment via `make deploy-all`. The smoke probe targets /healthz, /readyz, and /api/v1/health.

### Which databases does Fern Platform support?

PostgreSQL 14+ and Redis 6+ are the stated requirements, external or containerized. The Go module carries GORM drivers for both Postgres and SQLite, and the local compose stack runs postgres:14-alpine published on host port 55432 to avoid clashing with an existing local database.

### Which test frameworks have official Fern Platform clients?

Go with Ginkgo, Java with JUnit including a Gradle plugin, and JavaScript with Jest. The project also documents building your own client for Python, Ruby, PHP, .NET, or other languages, wiring up pytest, RSpec, PHPUnit, or NUnit.

## Sources

- [guidewire-oss/fern-platform on GitHub](https://github.com/guidewire-oss/fern-platform)
- [Issues](https://github.com/guidewire-oss/fern-platform/issues)
- [License: Apache-2.0](https://github.com/guidewire-oss/fern-platform/blob/main/LICENSE)
- [README](https://github.com/guidewire-oss/fern-platform/blob/main/README.md)
- [Releases](https://github.com/guidewire-oss/fern-platform/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/guidewire-oss-fern-platform
