API Implementation
For people who read or change the CLI's source: how pfy talks to the Manager's GraphQL API, how the code is laid out, and which operations exist. Using the CLI does not require any of it.
Architecture
The CLI is a Go program. GraphQL queries and mutations are written as Go structs with graphql field tags; the client library turns them into query text and decodes the response into the same structs.
Components
cli/
├── pfy/
│ └── main.go # Entry point; classifies errors and sets the exit code
└── internal/
├── api/ # Manager client
│ ├── client.go # GraphQL client with Bearer auth + REST pilet upload
│ ├── config.go # ~/.pfy/config.yaml loader (manager_url, token)
│ ├── operations.go # projects, tenants, applications, language packs, machine users
│ ├── multitenancy.go # environments, Tenant2Application enablement, config copy
│ ├── deployment.go # deployments and their specs
│ ├── service.go # application services
│ ├── datadep.go # managed data stores
│ ├── comms_config.go # communications config layers
│ └── proxy.go # proxy sync status / diff / sync
├── app/ # Commands
│ ├── app.go # Build metadata (Version, BuildTime; set via ldflags)
│ ├── commands.go # Root command, global flags, --version wiring
│ ├── cmd_*.go # One file per command group
│ ├── deployresolve.go # --env/--app/--tenant/--service -> Deployment resolution
│ ├── deploystack.go # nomad deploy --stack ordering
│ ├── output.go # -o text|json|yaml and stderr routing
│ ├── exit.go # exit codes (0, 1, 3, 4)
│ └── helpers.go # client construction, pagination, error classification
├── auth/ # Browser (device-flow) login
├── project/ # project.yaml handling and semver bumping
├── render/ # Deployment spec -> Nomad job HCL
├── adopt/ # Hand-written Nomad job -> deployment spec
├── platform/ # install.yaml -> platform stack (nomad / compose)
├── nomad/ # nomad CLI wrappers (validate, plan, run, inspect, legacy image rewrite)
├── dev/ # pfy dev: compose stack, seeding, dev proxy
├── pilet/ # pfy pilet create scaffolding
└── templates/ # Embedded CI/CD, Dockerfile, and Nomad job templatesGraphQL Client
Implementation
Built on hasura/go-graphql-client:
type Client struct {
gqlClient *graphql.Client
httpClient *http.Client
endpoint string
token string
}The client maintains both a GraphQL client for API operations and an HTTP client for REST endpoints (e.g., pilet uploads).
Authentication
Bearer token authentication via WithRequestModifier:
gqlClient = gqlClient.WithRequestModifier(func(r *http.Request) {
r.Header.Set("Authorization", "Bearer "+token)
})Type-Safe Operations
Struct-Based Queries
GraphQL operations are defined as Go structs with graphql tags:
func (c *Client) GetProjects(ctx context.Context, filters []Filter, order Order, pagination Pagination) ([]Project, error) {
var query struct {
Projects []Project `graphql:"projects(filters: $filters, order: $order, pagination: $pagination)"`
}
// ... execute query with variables
return query.Projects, nil
}Domain Types
The CLI defines domain types that map to GraphQL types:
type Project struct {
ID graphql.ID `graphql:"id" json:"id"`
Slug graphql.String `graphql:"slug" json:"slug"`
Name graphql.String `graphql:"name" json:"name"`
}
type Application struct {
ID graphql.ID `graphql:"id" json:"id"`
Slug graphql.String `graphql:"slug" json:"slug"`
Name graphql.String `graphql:"name" json:"name"`
Version graphql.String `graphql:"version" json:"version"`
}
// ... similar for Tenant, LanguagePackGraphQL Types
Type mapping from Go to GraphQL:
| Go Type | GraphQL Type | Usage |
|---|---|---|
graphql.String | String | Text fields |
graphql.ID | ID | Unique identifiers |
graphql.Int | Int | Integer numbers |
graphql.Boolean | Boolean | True/false values |
graphql.Float | Float | Decimal numbers |
All domain types use graphql tags for field mapping and json tags for JSON serialization.
Mutations
Mutations use input types:
type CreateProjectInput struct {
Slug graphql.String `json:"slug"`
Name graphql.String `json:"name"`
Description graphql.String `json:"description,omitempty"`
}
func (c *Client) CreateProject(ctx context.Context, input CreateProjectInput) (*Project, error) {
var mutation struct {
CreateProject Project `graphql:"createProject(input: $input)"`
}
// ... execute mutation
return &mutation.CreateProject, nil
}Application Copying
type CopyApplicationInput struct {
SourceApplicationID graphql.ID `json:"sourceApplicationId"`
TargetTenantID graphql.ID `json:"targetTenantId"`
Slug graphql.String `json:"slug"`
// ... other fields
}A PromoteApplicationInput type still exists in operations.go, but nothing sends it: pfy application promote refuses before making a request.
Implemented Operations
Projects
Queries:
GetProjects- List projects with filters, order, paginationGetProject- Get project by slugGetProjectByID- Get project by ID (used bypfy sync)
Mutations:
CreateProject- Create new projectUpdateProject- Update existing projectDeleteProject- Delete project
Tenants
Queries:
GetTenants- List tenants in a project (by project slug)GetTenant- Get tenant by project ID and slug
Mutations:
CreateTenant- Create tenant in projectUpdateTenant- Update tenantDeleteTenant- Delete tenantSetTenantDeploymentMode- Switch a tenant between shared and dedicated; returns the DNS checklist
Applications
Queries:
GetApplications- List applications in a project (by project slug)GetApplication- Get application by project ID and slug
Mutations:
CreateApplication- Create application in a projectUpdateApplication- Update name, description, and/or version (also whatupdate-versioncalls)DeleteApplication- Delete applicationCopyApplication- Copy an application to another tenant
Environments and enablement (multitenancy.go)
GetEnvironments,CreateEnvironment,UpdateEnvironment,DeleteEnvironmentGetTenant2Applications- List an application's tenant enablementsSetApplicationEnabledForTenant- Enable/disable an application for a tenantCopyTenant2ApplicationConfig- Copy per-tenant config between enablements
Deployments, services, data stores, comms, proxy
deployment.go- read deployments and update their spec (UpdateDeployment)service.go- list/create/update/delete an application's servicesdatadep.go- managed data stores and their backup policycomms_config.go- read/write the communications config of a cascade layerproxy.go- proxy sync status, rendered vhost preview, and the sync push
Language Packs
Queries:
GetLanguagePacks- List language packs in projectGetLanguagePack- Get a single language pack
Mutations:
CreateLanguagePack- Create language packDeleteLanguagePack- Delete language pack
Token check
VerifyToken- Runs a minimalprojectsquery; used bypfy whoamiand bypfy loginbefore saving a token
Filters, Ordering, and Pagination
Filter, Order, and Pagination
type Filter struct {
Field graphql.String `json:"field"`
Value graphql.String `json:"value"`
}
type Order struct {
Field graphql.String `json:"field"`
Direction graphql.String `json:"direction"` // "ASC" or "DESC"
}
type Pagination struct {
Offset graphql.Int `json:"offset"`
Limit graphql.Int `json:"limit"`
}Configuration Management
Config Structure
type Config struct {
ManagerURL string `yaml:"manager_url"`
Token string `yaml:"token"`
}Configuration is loaded from ~/.pfy/config.yaml and can be overridden by environment variables (PFY_MANAGER_URL, PFY_MANAGER_TOKEN). Those two keys are the whole schema.
Configuration Priority
- Command-line flags (highest priority)
- Environment variables
- Config file (lowest priority)
Error Handling
Each API call wraps its error with the operation name, and attaches the endpoint in an APIError:
if err := c.Query(ctx, &query, variables); err != nil {
return nil, fmt.Errorf("failed to get projects: %w", err)
}pfy/main.go then runs the error through app.ClassifyError once. HTTP 401/403 becomes a single token rejected by <url> ... sentence, other HTTP statuses become manager at <url> returned HTTP <code>: <body>, connection failures become cannot reach manager at <url> ..., and GraphQL errors are reduced to their messages (locations and paths are dropped). Unrecognised errors pass through unchanged.
Exit codes
exit.go defines four codes: 0 success, 1 any failure, and two verdict codes used only by pfy proxy status|diff|sync --exit-code: 3 (state is bad) and 4 (state cannot be determined, or LOCK_BUSY). A verdict is printed without the Error: prefix.
REST Endpoints
Pilet Upload
The client includes an UploadPilet method for uploading pilet packages via multipart/form-data, with the same Bearer token:
func (c *Client) UploadPilet(ctx context.Context, projectID, name, version, description, filePath string) error {
// Creates multipart form with file upload
// POST /pilets/{projectID}/{name}/{version}
}A non-2xx response is returned as upload failed with status <code>: <truncated body>.
Browser login
internal/auth implements the device flow used by pfy login: POST /api/cli/login/start with an ephemeral public key, then POST /api/cli/login/poll until the session is approved. The PAT comes back sealed to that key and is decrypted in memory.
Output
internal/app/output.go routes every list/get result through printResult. In text mode the command's own formatter runs; in json/yaml mode the typed result is marshalled to stdout and all human-readable chatter (printQuiet, printVerbose) goes to stderr. printData prints the data rows themselves and ignores --quiet.
Related
- Configuration: the settings this client reads
- Command Reference: the commands built on these operations