Skip to contents

Overview

This vignette describes the internal architecture of spdgt.auth for developers who build on or contribute to the package. If you just need to authenticate and make API calls, see vignette("authentication") and vignette("requests") instead.

State management

All authentication state lives in a single APIAuthState R6 object stored in the package environment at .state$auth. The object is created during .onLoad() and shared across all downstream packages via the spdgt.auth namespace.

# R/aaa.R — package environment
.state <- new.env(parent = emptyenv())

# R/zzz.R — created at load time
.onLoad <- function(libname, pkgname) {
  .state$auth <- init_APIAuthState()
}

Downstream packages (e.g., spdgt.sight) access this singleton by calling spdgt.auth::add_api() in their own .onLoad() to register endpoints. They never create their own APIAuthState.

Realms

A realm is a named group of APIs that share a single token. Two realms are configured:

Realm Token authority APIs
"counts" counts.spdgt.com counts, sightability, core lookups
"telemetry" marks.spdgt.com telemetry, depredation

Each realm stores its own token, refresh token, expiration, and auth type independently. Global user identity (user ID, email, project ID) is shared across realms and set by whichever realm authenticates first.

Realm OAuth configuration is stored in .realm_configs (defined in R/aaa.R), which maps each realm name to its OAuth client credentials, API key environment variable, and OIDC environment variable.

Registering APIs

add_api() registers an API endpoint and associates it with a realm:

# Creates the "counts" realm with base URL
add_api("counts", "https://counts.spdgt.com/api")

# Joins the existing "counts" realm — shares its token
add_api("sightability", "https://sight.spdgt.com/api", realm = "counts")

# Creates a separate "telemetry" realm
add_api("telemetry", "https://marks.spdgt.com/api")

Authentication flow

auth_login(realm) selects the authentication method based on the environment:

  1. OIDC environment variable set — uses the ShinyProxy-provided OIDC token (authenticate_oidc())
  2. API key environment variable set — uses the stored API key (authenticate_api_key())
  3. Interactive session — opens an OAuth 2.0 browser flow (authenticate_oauth())

Each method hits the realm’s /api/me endpoint to verify the token and retrieve user identity (ID, email, project ID). On success, credentials are stored in the realm’s slot within APIAuthState. Users do not need to call auth_login() directly. api_perform() calls it automatically when a request targets an unauthenticated realm.

Token storage per method

Method Token source Token format Stored as
OAuth Browser auth code flow Sanctum bearer token Bearer {token}
OIDC SHINYPROXY_OIDC_ACCESS_TOKEN env var Sanctum bearer token Bearer {token}
API key SPDGT_API_KEY env var api-key {key} Authorization: api-key {key}

Request pipeline

Every API call flows through the same pipeline. The high-level functions (api_get(), api_post(), etc.) compose these internal building blocks: api_init(), api_filters(), api_pagination(), api_includes(), api_appends(), and api_body().

api_init(api_name, endpoint)

Creates the base httr2 request object:

  1. Looks up the API’s base URL from APIAuthState
  2. Sets Accept and Content-Type to application/json
  3. Appends the endpoint path
  4. Tags the request with spdgt_api_name for realm resolution later
api_init("counts", "projects/age-classes")
# -> GET https://counts.spdgt.com/api/projects/age-classes

Request modifiers

Each modifier takes a request and returns a modified request, enabling piped composition:

  • api_pagination(req, pages) — translates list(size = 100) to ?page[size]=100
  • api_filters(req, filters) — translates list(species_id = 1) to ?filter[species_id]=1. Supports operators: ~ partial match, ! exclusion, !~ partial exclusion
  • api_includes(req, includes) — adds ?includes=relation1,relation2 for eager-loading relationships
  • api_appends(req, appends) — adds ?appends=field1,field2 for computed attributes
  • api_body(req, body) — serializes a tibble or list to a JSON request body, handling metadata nesting and validation

api_perform(req)

Executes the request with automatic authentication:

  1. Resolves the realm from the request’s spdgt_api_name tag
  2. Authenticates if the realm has no valid token
  3. Attaches the token via add_token() (Bearer for OAuth/OIDC, raw header for API key)
  4. Executes the request
  5. On 401, attempts a token refresh and retries once
  6. On persistent failure, calls abort_api_error() with structured diagnostics

Realm resolution

resolve_realm(req) determines which realm a request belongs to:

  1. Direct spdgt_realm tag (used by auth_me())
  2. spdgt_api_nameget_realm_for_api() lookup
  3. Default: "counts"

Composed example

A call like:

api_get("counts", "species",
  filters = list(name = "~Elk"),
  includes = "projects",
  pages = list(size = 50)
)

Expands internally to:

api_init("counts", "species") |>
  httr2::req_method("GET") |>
  api_pagination(list(size = 50)) |>
  api_filters(list(name = "~Elk")) |>
  api_includes("projects") |>
  api_perform()

Token refresh

When api_perform() receives a 401 response, it calls refresh_auth() which dispatches based on auth type: - OAuth — posts the refresh token to the realm’s /api/refresh endpoint - OIDC — posts the refresh token to the realm’s OIDC token endpoint

If refresh succeeds, the new tokens replace the old ones and the original request is retried. If refresh fails in an interactive session, OAuth falls back to a full re-authentication flow.

Response parsing

Parse functions convert raw httr2 responses into R data structures:

Function Input Output
parse_json2tibble() JSON with data + schema Tibble with type-coerced columns
parse_json2list() Any JSON R list
parse_multi2tibble() /multiple batch response Tibble with data, message, success columns
parse_parquet2df() Parquet bytes Tibble
parse_url() Export response Prints details, returns URL invisibly
parse_url2df() Export response Downloads parquet from URL, returns tibble

Schema-based type conversion

When validate = TRUE (the default), parse_json2tibble() uses the schema element in the API response to coerce columns to their correct R types. The schema maps column names to types like "integer", "string", "boolean", etc. See vignette("response-patterns") for the full type mapping.