CoMapeo Core for React Native
For managed Expo projects, please follow the installation instructions in the API documentation for the latest stable release. If you follow the link and there is no documentation available then this library is not yet usable within managed projects — it is likely to be included in an upcoming Expo SDK release.
For bare React Native projects, you must ensure that you have installed and configured the expo package before continuing.
npm install @comapeo/core-react-native
The library's AndroidManifest.xml sets two <application> attributes
that exclude the rootkey-bearing SharedPreferences from cloud backup
and device-to-device transfer:
<application
android:dataExtractionRules="@xml/comapeo_data_extraction_rules"
android:fullBackupContent="@xml/comapeo_backup_rules">If your host app's AndroidManifest.xml already declares either
attribute (a fairly common case in shipping apps), the manifest merger
will fail at build time with a "different value declared" error. The
fix is two steps:
-
Merge our exclusions into your existing rules XML. Add an
<exclude domain="sharedpref" path="comapeo-core.xml" />entry under both<cloud-backup>and<device-transfer>in yourdataExtractionRulesresource, and the same<exclude>under<full-backup-content>in yourfullBackupContentresource. The library's defaults are atandroid/src/main/res/xml/comapeo_data_extraction_rules.xmlandandroid/src/main/res/xml/comapeo_backup_rules.xmlfor reference. -
Tell the merger that your manifest wins. Add
tools:replaceto your app's<application>tag:<application xmlns:tools="http://schemas.android.com/tools" android:dataExtractionRules="@xml/your_app_extraction_rules" android:fullBackupContent="@xml/your_app_backup_rules" tools:replace="android:dataExtractionRules,android:fullBackupContent">
The rootkey is encrypted with a wrapper key from AndroidKeyStore.
That wrapper key is device-bound and non-exportable, so a backed-up
envelope is useless on any other device. The exclusion is
defense-in-depth (the encrypted blob shouldn't sit in cloud backups
even when it's useless to attackers) and UX (without the exclusion,
restore-to-new-device flows appear to succeed but then fail at first
launch with RootKeyException("Wrapper key alias missing"), which
is a confusing state to end up in).
Run npx pod-install after installing the npm package.
The module runs an offline-capable map server (@comapeo/map-server)
inside the embedded Node backend, served over loopback HTTP. Point a map
renderer such as MapLibre at the local URL to
draw background maps — including offline.
import { comapeoServicesClient } from "@comapeo/core-react-native";
// Available once the backend has started.
const baseUrl = await comapeoServicesClient.mapServer.getBaseUrl();
// → http://127.0.0.1:<port>
// Hand a style URL to your map renderer:
const styleUrl = `${baseUrl}/maps/fallback/style.json`;Three built-in map IDs are served under /maps/<id>/…:
fallback— a small offline map bundled with the module (@comapeo/fallback-smp); always available, used as the last resort.default— redirects to a hardcoded online style (demotiles.maplibre.org), so it needs a network connection.custom— an offline.smpthe user has imported through the app; returns 404 until one is added.
comapeoServicesClient is the client for the app-provided services RPC
(renamed from appRpcClient in @comapeo/ipc@9); mapServer is its
only member today.
The map server is plain HTTP on 127.0.0.1, which release builds block
by default. The Expo config plugin adds a loopback-scoped exception
so the server is reachable: an Android network-security-config limited
to 127.0.0.1/localhost, and the iOS NSAllowsLocalNetworking ATS
key. Traffic to the public internet keeps the secure default. If your
app manages its own networkSecurityConfig or App Transport Security
settings, make sure cleartext to loopback stays allowed.
New projects are created with no presets/categories unless you supply a
default config. Pass a .comapeocat file to the Expo config plugin; it
gets bundled into the app and applied to every project created without an
explicit config:
// app.config.js / app.json plugins
[
"@comapeo/core-react-native",
{
defaultConfig: "./assets/my-categories.comapeocat",
},
]The path is resolved relative to your app's project root. Omit
defaultConfig and new projects start empty. Use
@comapeo/default-categories
(or your own build) as the source .comapeocat — this module no longer
ships one.
If you later remove defaultConfig after having set it, run a clean
prebuild (expo prebuild --clean) so the bundled file is dropped from the
iOS Xcode project; a non-clean prebuild leaves the stale reference behind.
This module can forward its native-side and JS-side lifecycle events
into the host app's @sentry/react-native. Sentry is opt-in — if you
don't register the plugin and don't import the sub-export, no Sentry
code path is exercised and no DSN ends up in your APK/IPA. See
docs/ARCHITECTURE.md §7 for the
architectural overview and
docs/sentry-integration-plan.md
for the design plan and per-phase status.
@sentry/react-native is an optional peer dep of this module. Install
it in the host app and run Sentry.init(...) once at startup as
documented at https://docs.sentry.io/platforms/react-native/. The
runtime classes shipped with @sentry/react-native also satisfy the
Android FGS-process bridge — no extra Android dependency to declare.
In app.config.js (must be .js, not app.json, to read process.env):
export default {
expo: {
plugins: [
["@comapeo/core-react-native", {
sentry: {
dsn: process.env.SENTRY_DSN,
environment: process.env.SENTRY_ENVIRONMENT ?? "production",
// Optional: opt internal/test builds into the §9 capture-application-data
// toggle by default. Production stays off-by-default.
captureApplicationDataDefault:
(process.env.SENTRY_ENVIRONMENT ?? "production") !== "production",
// Optional: opt into Sentry structured logs on the
// Android FGS process. Pair with `enableLogs: true` in
// your host-app `Sentry.init(...)` (covers main-process
// Android + iOS).
enableLogs: process.env.SENTRY_ENVIRONMENT !== "production",
},
}],
],
},
};The plugin runs at expo prebuild and bakes the DSN, environment, and other
options into AndroidManifest meta-data and Info.plist keys. Sourcing values
from process.env lets EAS build profiles produce different builds without
code changes — see
docs/sentry-integration-plan.md §4.1
for the matching eas.json example with per-profile env vars.
import "@comapeo/core-react-native/sentry";That's it — importing the sub-export attaches the lifecycle listeners
to the host's already-initialised Sentry hub. No explicit handoff
call. As long as the host has run Sentry.init(...) (the
@sentry/react-native SDK reads its DSN from the same Info.plist /
manifest values your plugin wrote), errors and breadcrumbs flow
automatically. ERROR state transitions surface tagged with the
relevant phase (rootkey, starting-timeout,
node-runtime-unexpected, etc.); state transitions show up as
breadcrumbs that ride along on the next event.
This module owns the RN-side Sentry.init call. Do NOT call
Sentry.init yourself — call initSentry() once at app entry
and pass any allowlisted extensions through it:
import { initSentry } from "@comapeo/core-react-native/sentry";
import * as Sentry from "@sentry/react-native";
initSentry({
// Optional — append your own integrations to the defaults.
integrations: (defaults) => [
...defaults,
Sentry.reactNavigationIntegration(),
],
// Optional — runs AFTER this module's PII scrubber.
beforeSend: (event) => event,
// Optional — extra scope tags on the persistent global scope.
tags: { releaseChannel: "internal" },
});initSentry reads the plugin-baked DSN / environment / release /
sample rates from the native config and wires the RN, Node, and
Android-FGS hubs to the same values, so events from all three sides
land under one release / environment. Locked options (dsn,
release, environment, sampleRate, tracesSampleRate,
sendDefaultPii: false, enableLogs, user.id) come from the
plugin and can't be overridden by the host — TypeScript refuses them
at the call site. initSentry throws if the host already called
Sentry.init separately.
The same plugin-baked subset is also exported as sentryConfig
(empty {} when the plugin isn't registered) for read-only
inspection — e.g. logging which release the host is reporting under,
or rendering it in a debug screen — but it is NOT meant to be spread
into a separate Sentry.init call; initSentry is the supported
init entrypoint.
Once the plugin is registered with a dsn, the module captures
events from three layers, tagged for filtering in the dashboard:
layer:rn(JS adapter, auto-attached when the sub-export is imported) — state-machine ERROR transitions andmessageerrorparse failures; every state transition rides along as a breadcrumb.layer:native(Kotlin / Swift) —comapeo.boottransaction (root, force-sampled) with child spansboot.fgs-launch(Android only —startForegroundService→ FGS process ready),boot.extract-assets(Android only, first boot after install/ update — recursive copy ofnodejs-project/from APK assets to internal storage; iOS reads the bundle in place so no equivalent),boot.node-spawn(nodejs-mobile JNI call → controlstarted),boot.rootkey-load, andboot.init-frame. Plus state-transition breadcrumbs, control-frame breadcrumbs, watchdog/shutdown timeout events, rootkey-loadcaptureException. On Android adds FGS-lifecycle breadcrumbs.layer:node—boot.loader-init(process spawn → Sentry.init done),boot.import-index(aroundimport("./index.js")),boot.listen-control(control-socket bind),boot.manager-init(drizzle + SQLite + RPC bind), plus per-RPC method spans,handleFatalexceptions, anderror-nativeforwards from the embedded nodejs-mobile. Node-side spans inherit the FGS-side trace viaSentry.continueTraceon theboot.node-spawnspan ID forwarded as the--sentryTraceargv flag.@sentry/nodehas no offline transport, so its envelopes are forwarded over the control socket to the FGS-sidesentry-android(or sentry-cocoa on iOS) for queueing and send. Error events are deserialised into aSentryEventand captured viaSentry.captureEvent, which applies the native SDK's scope (device, OS, app, user, native breadcrumbs) at capture time so Node-emitted events end up with the same context as RN-side captures.
Each event also carries a proc tag for the actual OS process:
proc:main for everything on iOS (single-process), and
proc:main (RN code) or proc:fgs (anything in
:ComapeoCore — both the Kotlin FGS service and the embedded
nodejs-mobile) on Android.
The FGS-process Sentry SDK is initialised automatically in
ComapeoCoreService.onCreate from the manifest meta-data your
config plugin wrote. There's no extra configuration required for
multi-process Android apps using this module — that's the
SentryFgsBridge doing the work behind the scenes. If
@sentry/react-native isn't installed (so io.sentry.* isn't on
the runtime classpath), the bridge stays inert and the module
continues to function unchanged.
The Node-backend bundle (the loader.mjs spawn target plus its
dynamically-imported index.mjs, the import-in-the-middle hook
files, and the auto-emitted @sentry/node chunks) ships rolled-up +
minified, so without sourcemaps stack traces in Sentry are unreadable.
The bundle's sourcemaps ship inside the npm tarball with deterministic,
content-hashed Sentry debug IDs baked in at build time —
symbolication is keyed off the IDs, so you do not have to align this
module's version with your app's release.
Add one step to your release pipeline (after eas build, or as part
of the build's post-publish phase):
SENTRY_AUTH_TOKEN=… npx comapeo-rn-upload-sourcemaps \
--org your-org \
--project your-projectRe-uploading is idempotent: Sentry de-dupes by debug ID. The CLI
finds @sentry/cli via the transitive @sentry/react-native →
@sentry/cli chain in your node_modules; if you don't use
@sentry/react-native, add @sentry/cli to your devDeps yourself.
--targets <list> (default: all) restricts the upload to a subset of
android-debug, android-main, ios. --url points at a self-hosted
Sentry. SENTRY_ORG / SENTRY_PROJECT env vars work in place of the
flags.
Contributions are very welcome! See CONTRIBUTING.md for the development setup, how to run the tests, and the commit/PR/release conventions. For the architecture and a directory-by-directory breakdown see agents.md.