@oxy.so/app-preset
v3.2.1
Published
The Oxy distro of Expo — centralized Expo config plugin, Metro/Babel/ESLint factories, Tailwind base CSS and TypeScript bases shared by every Oxy app (Mention, Homiio, Allo, accounts, …). A zero-build package: apps replace copy-pasted config with a single
Readme
@oxy.so/app-preset
The Oxy distro of Expo — the shared configuration every Oxy app (Mention, Homiio, Allo, accounts, Commons, …) used to copy-paste, centralized into one zero-build package. Apps replace four config-plugin entries, a hand-tuned Metro config, a Babel config, an ESLint config, a Tailwind CSS header, and three tsconfigs with a single dependency, and pick up ecosystem changes with a version bump instead of re-editing every repo.
There is no build step: this package ships plain CommonJS config plus static CSS and JSON.
What it centralizes
| Piece | Import | Replaces |
| --- | --- | --- |
| Config plugin | ['@oxy.so/app-preset', {}] | iOS keychain entitlement + expo-build-properties + @oxy.so/services/plugins/withOxySharedPermissions |
| Android release build | @oxy.so/app-preset/plugin/withOxyAndroidRelease | R8 -optimize ProGuard file + shared-keystore release signing (opt-in, see below) |
| Android WebP resources | @oxy.so/app-preset/plugin/withOxyAndroidWebp | re-encoding generated mipmaps/splash bitmaps to real lossless WebP (opt-in, needs sharp) |
| Oxy Updates (OTA) | @oxy.so/app-preset/plugin/withOxyUpdates | the expo-updates manifest URL, release channel, runtimeVersion policy and the shared code-signing certificate (opt-in, see below) |
| Metro | @oxy.so/app-preset/metro | monorepo watch folders, block list, symlink + package-exports resolution, web-font/wasm asset exts, release minifier, NativeWind wrapper |
| Babel | @oxy.so/app-preset/babel | babel-preset-expo + module-resolver + react-native-worklets/plugin |
| ESLint | @oxy.so/app-preset/eslint | eslint-config-expo/flat + dist/* ignore |
| CSS base | @oxy.so/app-preset/base.css | Tailwind v4 + NativeWind + Bloom design-token imports + SDK @source globs |
| tsconfig | @oxy.so/app-preset/tsconfig/{base,frontend,backend}.json | the shared strict/composite TypeScript bases |
Usage
1. Config plugin (app.config.js / app.json)
plugins: [
// …app-specific plugins…
['@oxy.so/app-preset', {}],
]This adds the iOS keychain-access-groups entitlement
($(AppIdentifierPrefix)group.so.oxy.shared), the Oxy expo-build-properties
defaults (iOS deploymentTarget 16.4; Android compileSdk 36 / targetSdk 35 /
ProGuard + resource shrinking), and @oxy.so/services/plugins/withOxySharedPermissions:
the app declares and requests the signature-level so.oxy.permission.IDENTITY and
so.oxy.permission.DEVICE_SESSION, with <queries> for the Commons and Accounts
providers. That is how silent "Sign in with Oxy" works on Android: each Oxy app
has its own UID, and Commons — the only holder of the identity key — answers over
signature-protected IPC. Never add android:sharedUserId.
The iOS and build-property pieces are overridable:
['@oxy.so/app-preset', {
keychainGroup: 'group.so.oxy.shared', // false → skip iOS keychain entitlement
ios: { deploymentTarget: '17.0' }, // deep-merges over defaults; false → skip iOS build props
android: { targetSdkVersion: 34 }, // deep-merges over defaults; false → skip Android build props
}]The sharedUserId and sharedIdentityReader options were removed in 3.0.0 and
throw if passed. @oxy.so/services 11+ is a required peer.
webgpu: true (opt-in, 3.2.0+) also applies react-native-webgpu's config
plugin, for an app that draws with WebGPU on native. Install
react-native-webgpu in the app first; the option throws without it.
Android release-build plugins (opt-in)
These two are deliberately NOT part of ['@oxy.so/app-preset', {}]: one writes a
signing config and the other needs sharp installed, so both are registered
explicitly by apps that ship to Play.
plugins: [
// FIRST in the array so it RUNS LAST — mods execute in reverse registration
// order, and this has to run after every image generator (splash included).
'@oxy.so/app-preset/plugin/withOxyAndroidWebp',
// …app-specific plugins…
'@oxy.so/app-preset/plugin/withOxyAndroidRelease',
]withOxyAndroidRelease reads the release keystore from four Gradle properties
(OXY_UPLOAD_STORE_FILE, OXY_UPLOAD_STORE_PASSWORD, OXY_UPLOAD_KEY_ALIAS,
OXY_UPLOAD_KEY_PASSWORD) in ~/.gradle/gradle.properties or CI secrets — never
from the repo. When they are absent the release buildType falls back to the debug
keystore, so always verify the artefact's certificate (keytool -printcert
-jarfile <aab>, apksigner verify --print-certs <apk>) instead of assuming.
withOxyAndroidWebp requires sharp as a devDependency of the app.
Oxy Updates (OTA) (opt-in)
withOxyUpdates points expo-updates at the self-hosted Oxy Updates server
(/updates/v1 in oxy-api) and wires the ecosystem code-signing certificate. It is
opt-in because it needs the app's own registered client id.
plugins: [
['@oxy.so/app-preset/plugin/withOxyUpdates', { clientId: OXY_CLIENT_ID }],
]Prerequisites in the app: expo-updates as a dependency (at the version
expo install expo-updates resolves for its SDK), and a registered
ApplicationCredential publicKey (oxy_dk_...), normally already present as an
EXPO_PUBLIC_OXY_CLIENT_ID-backed constant because the same id identifies the app
to the session and OAuth flows. expo-updates needs no plugin entry of its own:
prebuild applies its config plugin automatically.
| Option | Default | Meaning |
| --- | --- | --- |
| clientId | required | The registered ApplicationCredential publicKey |
| apiOrigin | https://api.oxy.so | Origin serving /updates/v1 |
| channel | production | Release channel this binary polls |
| codeSigning | require | Fails when the certificate is missing; auto warns and false skips signing (development only) |
| certificatePath | bundled certificate | Only for a key-rotation overlap |
| runtimeVersionPolicy | appVersion | false leaves whatever the app declared |
runtimeVersion is the appVersion policy on purpose. It is the only policy the
Oxy toolchain resolves end to end: oxy-ship derives the publish runtime from
expo config --json --type public, where fingerprint resolves only to the
sentinel file:fingerprint (it is a build-time value), so a fingerprint app would
need --runtime-version passed by hand on every publish. The rule that follows:
the app version is the OTA compatibility boundary. Bump it whenever native
code changes. Installs on the old version then correctly stop receiving
updates published under the new one; installs on an unbumped version would
receive JS assuming native modules they do not have.
Code signing is one certificate for the whole ecosystem, shipped in this
package at certs/oxy-updates-code-signing.pem rather than copied into each app
repo, so a rotation is one preset bump. See certs/README.md for how it is
generated and rotated. Until that file exists, withOxyUpdates fails the build
by default rather than producing a binary that accepts unsigned manifests for
its whole lifetime. Development builds may explicitly pass
{ codeSigning: 'auto' } to receive platform warnings, or
{ codeSigning: false } to opt out of verification.
Publishing is oxy-ship (@oxy.so/ship); see that package's README and its
templates/publish-update.yml CI workflow.
2. Metro (metro.config.js)
const { createOxyMetroConfig } = require('@oxy.so/app-preset/metro');
module.exports = createOxyMetroConfig(__dirname, {
sharedTypesPackage: '@myapp/shared-types', // optional
cssInput: './global.css', // optional, this is the default
dropConsole: true, // optional, production web only
// svgTransformerPath: require.resolve('react-native-svg-transformer/expo'),
// extraBlockList: [path.join(__dirname, '../../.claude/worktrees')],
});workletsBundleMode: true (opt-in, 3.2.0+) turns on react-native-worklets'
Bundle Mode.
It wraps the finished config with worklets' getBundleModeMetroConfig. Use it
together with the Babel option of the same name (below); neither works alone.
extraBlockList accepts regular expressions or directory paths. Directory
paths are escaped and block only their descendants. extraNodeModules,
extraWatchFolders, extraAssetExts, and extraSourceExts extend the shared
defaults without replacing them.
3. Babel (babel.config.js)
module.exports = require('@oxy.so/app-preset/babel');With Bundle Mode (opt-in, 3.2.0+), a worker runtime runs the app's own modules:
module.exports = require('@oxy.so/app-preset/babel').createOxyBabelConfig({
workletsBundleMode: {
// The modules whose imports a worklet runs ON its runtime (worklets'
// `importForwarding`). An import not forwarded stays on the RN runtime,
// and a synchronous call to it from a worker runtime throws.
importForwarding: { relativePaths: ['components/map/engine/city3d'] },
},
});4. ESLint (eslint.config.js)
const oxyConfig = require('@oxy.so/app-preset/eslint');
module.exports = [...oxyConfig];5. CSS (global.css)
@import "@oxy.so/app-preset/base.css";
/* App-specific globs (later rules win on overlap): */
@source "./app/**/*.{js,jsx,ts,tsx}";
@source "./components/**/*.{js,jsx,ts,tsx}";6. TypeScript (tsconfig.json)
// frontend
{ "extends": "@oxy.so/app-preset/tsconfig/frontend.json", "compilerOptions": { "paths": { "@/*": ["./*"] } }, "include": ["**/*.ts", "**/*.tsx"] }
// backend
{ "extends": "@oxy.so/app-preset/tsconfig/backend.json", "compilerOptions": { "rootDir": "./", "outDir": "dist" }, "include": ["**/*.ts"] }Peer dependencies
All peers are optional except expo — a piece's peer is only needed when you
use that piece (e.g. expo-build-properties only when build properties are
enabled, eslint-config-expo only for the ESLint config). The config plugin and
factories throw a clear, actionable error if a required peer is missing.
Compatibility
Requires Expo SDK 56+; validated against Expo SDK 57 / React Native 0.86
(the current Oxy ecosystem target) via test-app-expo.
