A project stores its background layers one of two ways, and every rfp function that touches them works with either.
Split (default for new projects)
One GeoPackage per layer, under a background_layers/ directory:
<project>/
background_layers/
bcfishpass.streams_vw.gpkg
whse_basemapping.transport_line.gpkg
...
form_pscis.gpkg
<project>.qgsA change to one layer costs one file. Unchanged layers are never uploaded, adding or removing a layer is a file appearing or disappearing rather than a change Mergin cannot diff, and the worst case becomes the largest single layer instead of the whole project. Measured on a real project, adding a 28-row layer went from a 135 MB upload to 135 KB.
The directory keeps rfp's files together and out of the project root, which belongs to the user - real projects hold 36 to 60 root entries of kml, spreadsheets, rasters and ad-hoc analysis GeoPackages that people browse to find their own work.
Monolith (existing projects)
Every layer in a single background_layers.gpkg. Projects created before
this layout existed keep working unchanged; convert one with
rfp_project_split() when you choose to.
Choosing and naming
Layout is detected from the project directory. Pass layout to state it
outright - rfp_project_create() needs that, since nothing exists yet to
detect. Names resolve argument first, then session option, then default:
rfp.dir_layersdirectory holding per-layer GeoPackages, default
"background_layers"rfp.file_gpkgsingle-GeoPackage name, default
"background_layers.gpkg"rfp.fn_layer_filefunction mapping a layer name to its file name, default
function(layer) paste0(layer, ".gpkg")
So dir_layers = "gpkg_layers" on a single call, or
options(rfp.dir_layers = "gpkg_layers") for a session, and the layer to
file mapping is a function rather than a pattern - <schema>.<table>.gpkg
is the default convention, not an assumption baked into callers.
Resolving it yourself
rfp_layout() returns the resolved spec - which layout is in force, and the
names it resolved. rfp_layout_layers() lists what the project holds, either
way, and rfp_layout_layer_path() says which file a layer maps to. These are
exported so a consumer outside rfp reads a project the way rfp does:
spec <- rfp_layout(project_dir)
spec$kind # "split" or "monolith"
layers <- rfp_layout_layers(spec) # data frame: layer, file
unique(layers$file) # every rfp-managed GeoPackage
rfp_layout_layer_path(spec, layers$layer[1])sf::st_layers(<one file>)$name is what this replaces, and it cannot see a
split project at all.
What the split does not change
Refresh changesets were already small; they stay small, because stable feature ids do that work and geodiff runs per file either way. And a schema change still cannot be diffed - it costs that layer's file rather than the project's, so splitting bounds the blast radius without removing it.
