Lesson 18 of 26
Permissions and Native Modules
Why requesting camera, location or notification access is async and runtime-only on both platforms, and what a native module actually is under the hood.
Almost every non-trivial mobile app eventually needs something the JavaScript engine cannot provide on its own: the camera, the user's precise location, push notifications, contacts. Getting access to any of those touches two related but distinct ideas — permissions, and native modules — and both are worth understanding conceptually before reaching for whichever specific library your project uses to handle them.
Permissions are asynchronous and requested at runtime
A permission is the OS asking the user, on the app's behalf, "is it okay for this app to do this specific thing?" Three properties of that are true on both iOS and Android, and worth internalizing before writing any code against a specific permissions library:
- It's asynchronous. Requesting a permission is never instant — it involves showing a system prompt (or checking a previously stored answer), so the request itself is always some kind of promise or callback-based API, never a value you can read synchronously.
- It's requested at runtime, not just declared ahead of time. Declaring
in
Info.plist(iOS) or the Android manifest that your app might use the camera is necessary, but it isn't sufficient — the OS still shows the user an explicit prompt the first time your code actually asks, and nothing is granted until the user responds to that prompt. - It can be denied, and it can be revoked later. A user can say no the first time, or say yes and later revoke it from system settings, with no notice to your app. Correct code treats "do I currently have permission" as a question to check fresh, not a fact learned once and remembered forever.
// A conceptual shape — the exact API differs by library, but this pattern
// (check, then request if needed, then branch on the result) is universal
async function ensureCameraAccess() {
const current = await checkCameraPermission();
if (current === "granted") return true;
if (current === "denied-permanently") return false;
const result = await requestCameraPermission();
return result === "granted";
}Both platforms also require you to say why you want the permission,
before the user ever sees the prompt. iOS requires a usage-description
string in Info.plist (for example, an NSCameraUsageDescription entry
with a human-readable sentence) — omit it, and the app crashes the moment
it tries to request that permission, rather than silently failing. Android
requires the permission itself to be declared in the app's manifest, with
its own runtime request flow for anything considered sensitive. The exact
mechanics differ, but the underlying requirement is the same on both
platforms: declare intent up front, and explain it to the user, before the
OS will even offer the choice.
Native modules: how JS reaches what the JS engine can't
Permissions gate access to a capability; a native module is the mechanism that actually lets your JavaScript use that capability once access is granted. The JavaScript engine running your app's code is just that — a JS engine. It has no built-in way to talk to a Bluetooth radio, or read raw sensor data, or trigger a native share sheet. A native module is real platform code (Swift or Objective-C on iOS, Kotlin or Java on Android) that exposes a JS-callable API bridging into functionality that genuinely requires native access:
// Conceptual — calling into native code through a module's JS-facing API
import { NativeCameraModule } from "some-camera-library";
async function takePhoto() {
const photo = await NativeCameraModule.capture();
return photo.uri;
}From the JS side, calling a native module often looks exactly like calling any other async function — that's the whole point of the abstraction. Under the hood, the call is being routed to real native code, executed there, and the result routed back to JS (the bridge/JSI lesson later in this part goes into exactly how that routing works).
You will rarely write one yourself
The overwhelming majority of everyday React Native apps never write a native module from scratch. Common needs — camera, location, secure storage, biometrics, push tokens, contacts — already have a maintained community package or an Expo-provided module wrapping the native code for you, exposing a clean JS API on top. Writing your own native module is real work reserved for genuinely novel native functionality with no existing wrapper: a proprietary hardware SDK, a highly specialized native library, something nobody has needed enough to publish yet. For nearly everything else, the practical skill is choosing and correctly using an existing module, not authoring native code by hand.
What to remember
- Permission requests are asynchronous, must happen at runtime (a manifest/plist declaration alone is never enough), and can be denied or revoked later — never assume a past grant still holds.
- Both platforms require you to explain why you want a permission before the OS shows the prompt:
Info.plistusage-description strings on iOS, manifest declarations on Android. - A native module is how JS reaches functionality the JS engine can't do alone, by calling into real Swift/Objective-C or Kotlin/Java code.
- Most everyday apps use existing community or Expo-provided native modules rather than writing their own — writing one from scratch is reserved for genuinely novel native needs.
Check yourself
4 questions · pass 3/4 to unlock The Bridge, JSI, and the Three Threads
1.Why must a permission like camera or location access be requested at runtime, rather than it being enough to just declare it in Info.plist or the Android manifest?
2.A user granted your app camera permission last year. What can happen to that permission later, that your code needs to account for?
3.What is a native module, conceptually?
4.On iOS, what must an app provide before it can even show the system permission prompt for something like camera access?
4 left to answer