arrow_back Back to Blog
capacitor camera lovable mobile app capacitor

Capacitor Camera for Lovable Apps

·

Almost every Lovable app we wrap for the stores has a photo upload somewhere. Profile pictures, receipts, a listing photo, a damage report. And almost every one of them does it the same way Lovable generated it: an <input type="file" accept="image/*"> hidden behind a nice button.

Good news first. Inside a Capacitor app, that input mostly keeps working. On iOS it shows the system sheet with Take Photo, Photo Library and Choose File. On Android you get a chooser. So the first question isn’t “how do I add the Capacitor camera plugin.” It’s whether you need it at all.

My answer: for a profile picture, often not. For anything where people take a lot of photos, yes. Here’s why, and the traps either way.

The crash nobody sees in the simulator

Before anything else, add the iOS privacy strings. Open ios/App/App/Info.plist and add these three keys:

<key>NSCameraUsageDescription</key>
<string>Take a photo of your receipt to attach it to an expense.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Choose a photo of your receipt from your library.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Save receipt photos you take to your library.</string>

Without NSCameraUsageDescription, iOS doesn’t show an error or a permission prompt. It kills the app the moment the camera is requested. This applies to the plain file input too, because tapping Take Photo in that sheet opens the camera. The simulator has no camera, so people test on the simulator, ship to TestFlight, and the first real tester’s app closes on them.

The wording matters as well. Apple rejects vague purpose strings like “This app needs camera access” under guideline 5.1.1. Say what the photo is for, in your app’s terms. Receipts, listings, whatever it actually is.

Why we switch to the plugin anyway

We wrapped an inspection app last year. Field staff walked round properties and attached 20 to 40 photos per report. On the web it was fine, because they used it on laptops with files from a camera. Once it became a phone app, every photo was a straight-off-the-sensor iPhone image, roughly 3 to 5 MB each. Uploads over patchy 4G took minutes, a few timed out halfway, and the Supabase Storage usage graph went vertical in the first week.

The file input gives you whatever the camera produced. The Capacitor camera plugin lets you resize on the device before a single byte leaves the phone. That’s the main reason to use it. We set the longest side to 1600 px and quality to 80, and photos dropped to around 300 KB each. Nobody looking at a damaged skirting board on a phone screen could tell the difference.

Other reasons we reach for it: you want the camera to open directly without the three-option sheet, you want multi-select from the gallery with a limit, or you’re worried about Apple’s 4.2 “this is just a website” review and want genuinely native features in the build. It helps with that last one, a little. It won’t save an app that’s otherwise a thin wrapper.

Setting it up

npm install @capacitor/camera
npx cap sync

One thing that trips people up: the API changed in plugin 8.1. Most tutorials, and most code an AI will write for you, use Camera.getPhoto() with a resultType of Uri, Base64 or DataUrl. That’s deprecated now. It was split into takePhoto() and chooseFromGallery(), and the result always gives you a webPath you can load in the WebView.

import { Camera } from '@capacitor/camera';

const photo = await Camera.takePhoto({
  quality: 80,
  targetWidth: 1600,
  targetHeight: 1600,
  correctOrientation: true,
});

targetWidth and targetHeight must be set together, or neither. correctOrientation is already true by default, I just like seeing it. Rotated photos are the second most common bug report on camera features, and it’s always because someone turned this off for speed.

For the gallery, chooseFromGallery({ allowMultipleSelection: true, limit: 10 }). The limit is honoured on iOS and Android 13+. On older Android the user can pick as many as they like, so check the length yourself.

On Android 11 and up (with Google system updates), picking from the gallery goes through the system Photo Picker and needs no storage permission at all. You only need the old READ_EXTERNAL_STORAGE / WRITE_EXTERNAL_STORAGE permissions if you set saveToGallery: true, and those can be capped at SDK 32. Don’t add them “just in case.” Google Play asks pointed questions about broad media permissions now, and you don’t want to fill in that declaration form for a feature you aren’t using.

Getting the photo into Supabase Storage

Lovable apps store files in Supabase Storage, so the last step is the same whether you’re on Lovable Cloud or your own project. webPath is a URL the WebView can fetch, so turn it into a Blob and upload that:

const blob = await (await fetch(photo.webPath)).blob();
const path = `${user.id}/${crypto.randomUUID()}.jpg`;
const { error } = await supabase.storage
  .from('receipts')
  .upload(path, blob, { contentType: 'image/jpeg' });

Put the user’s id at the start of the path. Your storage RLS policies can then check (storage.foldername(name))[1] = auth.uid()::text, which is the usual pattern and the one Lovable tends to generate when it gets policies right. Check that it did. We find buckets with no upload policy, or an insert policy for any authenticated user on any path, more often than I’d like.

Handle “cancel” as a normal thing

Users open the camera and back out constantly. The plugin treats that as an error (code OS-PLUG-CAMR-0006 on native for a cancelled photo, OS-PLUG-CAMR-0003 when camera access is denied). If your Lovable code wraps the call in a generic try/catch that shows “Something went wrong” in a red toast, every cancel looks like a crash. Swallow the cancel quietly. For a denied permission, tell them where to switch it on in Settings, because the system won’t ask a second time.

The web build still works with the same code: takePhoto() falls back to a file input in the browser, so you don’t need two versions of the upload screen.

If your Lovable app needs to get into the App Store and Google Play with things like this done properly, that’s what we do. The fixed-price packages are on the home page.