Package internals
internals.RmdOverview
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:
-
OIDC environment variable set — uses the
ShinyProxy-provided OIDC token (
authenticate_oidc()) -
API key environment variable set — uses the stored
API key (
authenticate_api_key()) -
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.
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:
- Looks up the API’s base URL from
APIAuthState - Sets
AcceptandContent-Typetoapplication/json - Appends the endpoint path
- Tags the request with
spdgt_api_namefor realm resolution later
api_init("counts", "projects/age-classes")
# -> GET https://counts.spdgt.com/api/projects/age-classesRequest modifiers
Each modifier takes a request and returns a modified request, enabling piped composition:
-
api_pagination(req, pages)— translateslist(size = 100)to?page[size]=100 -
api_filters(req, filters)— translateslist(species_id = 1)to?filter[species_id]=1. Supports operators:~partial match,!exclusion,!~partial exclusion -
api_includes(req, includes)— adds?includes=relation1,relation2for eager-loading relationships -
api_appends(req, appends)— adds?appends=field1,field2for 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:
- Resolves the realm from the request’s
spdgt_api_nametag - Authenticates if the realm has no valid token
- Attaches the token via
add_token()(Bearer for OAuth/OIDC, raw header for API key) - Executes the request
- On 401, attempts a token refresh and retries once
- On persistent failure, calls
abort_api_error()with structured diagnostics
Realm resolution
resolve_realm(req) determines which realm a request
belongs to:
- Direct
spdgt_realmtag (used byauth_me()) -
spdgt_api_name→get_realm_for_api()lookup - 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.