# Upgrading your Project Source: https://docs.a0.dev/advanced/upgrading-your-project Learn why and how to upgrade your a0 project ## Why upgrades are necessary To enable new capabilities and ensure compatibility with our mobile app, you need to upgrade your a0 project periodically. Our mobile app only supports the latest version of a0 so you need to update inorder to test your app. ## Runtime version and Expo SDK a0 projects are built on top of Expo. In a0, you'll see a setting called **runtime version**, which is based on the Expo SDK version. When we upgrade a0 versions, the runtime version (and underlying Expo SDK version) in your project is also upgraded. The runtime version determines which Expo APIs, modules, and features are available to your app. The latest a0 version uses Expo SDK 54 as of 11/18/25. ### Understanding runtime versions Each runtime version includes: * **Core Expo modules**: Camera, Location, Notifications, and other built-in capabilities * **React Native version**: The underlying React Native framework version * **Native module support**: Compatibility with native iOS and Android modules * **API availability**: Which Expo APIs you can use in your project ### How runtime version upgrades work When you upgrade your a0 project: 1. **Runtime version updates**: Your project's runtime version is upgraded to match the latest supported version 2. **Package versions change**: All Expo-related packages are updated to versions compatible with the new runtime version 3. **React Native may update**: The underlying React Native version may change to match the Expo SDK requirements 4. **Native modules update**: Native iOS and Android modules are updated to work with the new runtime version ## When to upgrade The preview on the website will prompt you when it's time to upgrade your project. You won't be able to create new builds without upgrading to the latest version of a0. ## How to upgrade ### Upgrade from the UI prompt When an upgrade is available, you'll see a prompt in the UI. Simply follow the on-screen instructions to upgrade your project. The upgrade prompt will appear automatically when a new version is available and your project needs to be updated. ### Upgrade from Build Settings You can also manually upgrade your runtime version from the Build Settings tab: 1. Navigate to your project's **Build Settings** tab 2. Find the **Runtime version** section 3. Select the latest available runtime version 4. Save your changes You can only switch between the runtime version that was the latest when your project was created and the latest version. Intermediate runtime versions are not available for selection. ## Over-the-air updates Over-the-air (OTA) updates allow you to push code changes to your app without requiring users to download a new version from the app store. However, there's an important limitation to understand. ### OTA update limitations Over-the-air updates only work **within the same runtime version** that your app was built on. This means: * **Code changes**: You can push JavaScript and asset updates via OTA within the same runtime version * **Version upgrades**: You cannot upgrade the runtime version using OTA updates * **New builds required**: To get all users on the latest runtime version, you must release a new build to the App Store or Play Store OTA updates are great for quick bug fixes and feature updates, but they cannot change the underlying runtime version. ### When to release a new build You'll need to create and release a new build to app stores when: * You've upgraded your project to a new runtime version * You change your projects build settings like App Icon or permissions, If you upgrade your project to a new runtime version but don't release a new build, users with older app versions won't receive your over-the-air update. ## Why your app might be different after upgrading If you notice changes in your app's functionality after upgrading, this is expected behavior. When we upgrade versions, the runtime version and the versions of packages in your app change, which can introduce changes in functionality. The runtime version upgrade is a significant part of this process, as it updates the core framework your app runs on, including React Native and all Expo modules. Package version updates often include bug fixes, performance improvements, and new features that enhance your app's capabilities. Breaking changes or significant behavioral tweaks may require you to update your code to maintain the same functionality. Always test your project after upgrading it and make changes as necessary. # New Chat Source: https://docs.a0.dev/agent-guide/new-chat-explained ## What is a New Chat? When you see "Chat history is too long", creating a new chat resets your conversation history while preserving your entire app. Your files, builds, and project settings remain unchanged. ## Why Do I Need a New Chat? Chat histories can become very long over time. When they get too large, they can cause performance issues. Starting a new chat: * **Fixes:** Chat loading and sending issues * **Improves:** Response speed and reliability * **Maintains:** Full access to your app and project ## How to Create a New Chat For an Existing Project: 1. Navigate to your project 2. Click the **"New Chat"** button in the header Image7 31 25at5 02PM JPE ## What You Keep with a New Chat When you create a new chat for an existing project, you keep **everything** important: * **All your app files** (code, images, configuration) * **Your app builds** and deployment history * **Project settings** and integrations * **App Store Connect** configurations * **Supabase** database connections * **All your published versions** ## Key Difference **Resets:** Only your conversation history with the AI\ **Keeps:** Your entire app, files, builds, and project settings **Creates:** A completely new project from scratch\ **Resets:** Everything. You start with a blank slate ## Common Questions **No!** Your app, files, and all progress remain completely intact. Only the conversation history resets. **Yes!** The AI can access all your current project files in the new chat, so it understands your app's current state. **All preserved!** Your build history, deployed versions, and App Store submissions remain unchanged. After about 15-20 messages (or 80,000 characters), or when the AI is making the same mistakes repeatedly, seems confused about your project structure, or you're switching to completely different features. The AI can see all your current files in the new chat, so you can immediately ask it to continue working on features or fix issues without any setup. # null Source: https://docs.a0.dev/agent-guide/overview ## Why this matters After analyzing millions of chats, we've identified several patterns that are stopping users from completing their app. This guide breaks down exactly how to talk to a0.dev to get what you want. ## What you'll learn The three things that really matter when sending a message How to write prompts that get results, examples included When and why to start a new chat while keeping your app # null Source: https://docs.a0.dev/agent-guide/three-principles # Summary of the Principles "Make it better" is not a prompt. List exactly what you want. One message should contain 3-5 small tweaks OR one big feature. Not both. When something breaks, just copy in the error. Messages like 'fix the error' will likely not get you anywhere. After 15-20 messages, the AI gets confused. Starting a new chat helps it continue on your app with a clear context. ## 1. Be Very Specific A clear and articulated success criteria helps our agent deliver the best results that make you happy. ```text Good theme={null} Update the login page: 1. Change button color from blue to #22C55E 2. Add "Remember me" checkbox below password field 3. Center the form vertically on the page 4. Add loading spinner when submitting ``` ```text Bad theme={null} Make the login page better ``` ## 2. Show It The Error When something breaks the easiest things to do is just copy the error from: 1. The bottom of the error screen within the app preview 2. Beneath the app preview inside the console logs ## 3. Make a New Chat After about 15-20 messages (or 80,000 characters) our agent starts degrading in performance. A good set of rules for when to use a new chat is if: * The AI is making the same mistakes repeatedly * It seems confused about your project structure * You're switching from one feature to something completely different * The conversation has become a debugging nightmare Apps can contain unlimited chat sessions # null Source: https://docs.a0.dev/agent-guide/writing-prompts ## The anatomy of a good prompt Every effective prompt has these elements: ### 1. Clear scope State exactly what you want to change. Instead of "improve the form" try "add email validation to the signup form." ### 2. Specific details Include colors, positions, text, behavior. Instead of "add a button" try "add a green 'Continue' button below the form that navigates to /dashboard." ### 3. Context when needed If it's a bug, include the error. If it's a feature, explain where it fits in the flow. ### 4. One goal per message Try not to combine "fix the login bug AND redesign the homepage AND add a new API endpoint." Picking one usually means it gets completed thoroughly. ## Examples that actually work These prompts get results because they're clear, specific, and actionable. ```text theme={null} I need to update the login screen: 1. Change the button color from blue to green 2. Add a "Forgot Password?" link below the login button 3. Make the email input field show a keyboard with @ symbol 4. Add validation to show error message if email format is wrong ``` ```text theme={null} Add a new settings screen with these options: - Toggle for push notifications - Dark mode switch - User profile picture upload - Logout button at the bottom ``` ```text theme={null} The profile save button throws this error: TypeError: Cannot read property 'id' of undefined at ProfileScreen.js line 45 Please fix by checking if user exists before accessing user.id ``` ```text theme={null} Build a tally counter app with the key output being UI design quality. This app should have two components: - The home page where users see all of their tallied items (quick +/- in this UI) - The create tally item screen/modal which allows the user to set the name and other metadata Lets build this in the neobrutalism style with a cartoonish minimalism design. ``` If you have an image or design reference, you can attach it to the chat! This helps the AI understand exactly what you're looking for visually. # Android FCM Setup Source: https://docs.a0.dev/development/android-fcm-setup Set up Firebase Cloud Messaging credentials for Android push notifications # Android FCM Setup a0 uses Firebase Cloud Messaging (FCM v1) to deliver push notifications to Android devices. You need two files from Firebase: | File | Purpose | | -------------------------------- | ------------------------------------------------------------------------------ | | **google-services.json** | Registers your Android app with Firebase. Injected into the APK at build time. | | **Firebase service account key** | Private key used by a0's servers to send notifications via the FCM v1 API. | ## Step 1: Create a Firebase project If you already have a Firebase project for your app, skip to Step 2. 1. Open the [Firebase Console](https://console.firebase.google.com/) 2. Click **Add project** and follow the setup wizard Creating a new Firebase project ## Step 2: Download google-services.json This file contains your Firebase project's public configuration and is required for your Android app to connect to Firebase. 1. In the [Firebase Console](https://console.firebase.google.com/), open **Project settings** (gear icon) 2. Under **Your apps**, select your Android app (or click **Add app** → **Android** if you haven't added one yet) 3. Enter your Android package name — this should match the bundle identifier used in your a0 project 4. Click **Download google-services.json** Downloading google-services.json from Firebase Console This file contains public-facing identifiers and is safe to commit to your repository. It does **not** contain any private keys. ## Step 3: Create a Firebase service account key The service account key is a private JSON file that authorizes a0's servers to send push notifications on your behalf via the FCM v1 API. 1. In the Firebase Console, open **Project settings** → [**Service accounts**](https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk) 2. Make sure **Firebase Admin SDK** is selected Firebase Service accounts settings 3. Click **Generate New Private Key** 4. Confirm by clicking **Generate Key** — a JSON file will download Generating a new private key Store this file securely. It contains a private key that grants access to send notifications to your app. **Do not** commit it to your repository. ### Using an existing service account If you already have a Google Cloud service account and want to reuse it for FCM: 1. Open the [IAM Admin page](https://console.cloud.google.com/iam-admin/iam) in Google Cloud Console 2. Find the service account you want to use and click the pencil icon to **Edit Principal** IAM Admin page in Google Cloud Console 3. Click **Add Role** and select **Firebase Cloud Messaging API Admin** Adding Firebase Messaging API Admin role 4. Click **Save** 5. Download a new JSON key for this service account if you don't already have one ## Step 4: Upload to a0 1. Open your project in a0 2. Go to **Project Settings** → **Build Settings** → **Android Credentials** 3. Upload your **google-services.json** file 4. Upload your **Firebase service account** key file 5. Build a new custom APK — the credentials will be included automatically Both files will show a **Configured** badge once uploaded. You can replace or delete them at any time. After uploading new credentials, you need to create a new APK build for the changes to take effect. Existing builds will continue using the credentials they were built with. ## Helpful links * [Firebase Console](https://console.firebase.google.com/) * [Firebase Service Accounts](https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk) * [Google Cloud IAM Admin](https://console.cloud.google.com/iam-admin/iam) * [FCM v1 API documentation](https://firebase.google.com/docs/cloud-messaging/migrate-v1) * [Push Notifications guide](/development/push-notifications) # Deep Links Source: https://docs.a0.dev/development/deep-links Configure URL schemes to enable deep linking into your a0 app # Deep Links Deep links allow users to open your app directly from a URL, enabling features like email verification links, social sharing, and custom integrations. Deep links use URL schemes (like `myapp://`) to open your app and optionally navigate to specific content within it. ## Overview When you configure deep link schemes in your `build.yaml`, a0 automatically: 1. Registers the URL schemes with iOS (Info.plist) 2. Adds intent filters to Android (AndroidManifest.xml) 3. Configures Expo to recognize the schemes at runtime Your app's bundle ID is automatically added as a scheme, so you always have at least one working deep link scheme without any configuration. ## Configuration Add the `schemes` property to your `.a0/build.yaml` file: ```yaml theme={null} general: runtimeVersion: "exposdk:54.0.0" schemes: - myapp - myapp-dev ios: versionName: "1.0.0" android: versionName: "1.0.0" ``` Schemes must be lowercase and start with a letter. They can only contain letters, digits, plus signs (+), hyphens (-), and dots (.). ## Handling Deep Links Use the `expo-linking` library to handle incoming deep links in your app. ### Basic Setup ```tsx theme={null} import * as Linking from 'expo-linking'; import { useEffect } from 'react'; export default function App() { useEffect(() => { // Handle deep link when app is already open const subscription = Linking.addEventListener('url', ({ url }) => { handleDeepLink(url); }); // Handle deep link that opened the app Linking.getInitialURL().then((url) => { if (url) { handleDeepLink(url); } }); return () => subscription.remove(); }, []); const handleDeepLink = (url: string) => { const parsed = Linking.parse(url); console.log('Deep link received:', parsed); // Navigate based on parsed.path and parsed.queryParams }; return ( // Your app content ); } ``` ### Creating Deep Link URLs Generate URLs that open your app: ```tsx theme={null} import * as Linking from 'expo-linking'; // Create a URL with your custom scheme const url = Linking.createURL('profile/123', { scheme: 'myapp', queryParams: { ref: 'email' } }); // Result: myapp://profile/123?ref=email ``` ### With React Navigation If you're using React Navigation, configure deep linking in your navigation container: ```tsx theme={null} import { NavigationContainer } from '@react-navigation/native'; const linking = { prefixes: ['myapp://', 'myapp-dev://'], config: { screens: { Home: '', Profile: 'profile/:id', Settings: 'settings', }, }, }; export default function App() { return ( {/* Your navigators */} ); } ``` ## Testing Deep Links Use the `xcrun` command to test deep links on iOS: ```bash theme={null} xcrun simctl openurl booted "myapp://profile/123" ``` Use `adb` to test deep links on Android: ```bash theme={null} adb shell am start -a android.intent.action.VIEW -d "myapp://profile/123" ``` The easiest way to test on a physical device: 1. Send yourself a message (email, Slack, etc.) containing the deep link URL 2. Tap the link to open your app ## Common Use Cases Send users a verification link that opens your app and marks their email as verified: ```tsx theme={null} // In your email: myapp://verify?token=abc123 const handleDeepLink = (url: string) => { const { path, queryParams } = Linking.parse(url); if (path === 'verify' && queryParams?.token) { verifyEmail(queryParams.token); } }; ``` Allow users to reset their password from an email link: ```tsx theme={null} // In your email: myapp://reset-password?token=xyz789 const handleDeepLink = (url: string) => { const { path, queryParams } = Linking.parse(url); if (path === 'reset-password' && queryParams?.token) { navigation.navigate('ResetPassword', { token: queryParams.token }); } }; ``` Let users share specific content that opens directly in your app: ```tsx theme={null} // Shared link: myapp://post/456 const handleDeepLink = (url: string) => { const { path } = Linking.parse(url); const match = path?.match(/^post\/(\d+)$/); if (match) { navigation.navigate('Post', { id: match[1] }); } }; ``` Track referrals by including a code in the deep link: ```tsx theme={null} // Referral link: myapp://signup?ref=friend123 const handleDeepLink = (url: string) => { const { queryParams } = Linking.parse(url); if (queryParams?.ref) { // Store referral code for attribution storeReferralCode(queryParams.ref); } }; ``` ## Automatic Bundle ID Scheme Your app's bundle ID (e.g., `dev.a0.apps.myapp`) is automatically registered as a URL scheme. This ensures you always have a unique, collision-free scheme available even without explicit configuration. While the bundle ID scheme works, custom schemes like `myapp://` are shorter and more user-friendly for sharing. # Payment Implementation Source: https://docs.a0.dev/development/monetization/payment-implementation Integrate subscriptions and one-time purchases into your a0 app with code examples # Payment Implementation Learn how to implement subscriptions and one-time purchases in your a0 app using the `a0-purchases` library. The a0 payment system uses the [`a0-purchases`](https://www.npmjs.com/package/a0-purchases) library. All apps built with a0 come with the necessary configuration and providers built in. ## Platform support and testing (important) * Web: Stripe (test and production). You can test purchases in the browser. * iOS: Apple App Store. You sync products to App Store Connect and test purchases via TestFlight. * Android: not supported yet. Apple payments do not work in the a0 app or Expo Go. To test iOS purchases, you must use TestFlight or a native iOS build. ## Prerequisites Before implementing payments in your code, ensure you have: Defined your features and plans (either in the dashboard UI or by editing `.a0/monetization.yaml` via the Monetization tab), and created at least one offering. See the [Payment Setup](/development/monetization/payment-setup) guide. Synced your plans to Stripe and/or Apple App Store Connect. If you're **migrating an existing iOS subscription**, link your existing StoreKit product IDs in `.a0/monetization.yaml` (v2) so your paywall can resolve products immediately (without waiting on a successful first sync). Created at least one offering and set it as current - this is required for paywalls to display products. If you're using the a0 coding agent: it can edit `.a0/monetization.yaml` directly (same as `build.yaml` and `general.yaml`), then sync to Apple/Stripe. It should not tell you to do it yourself. ## One-time purchases (lifetime unlocks + consumables) The same paywall + `purchase(packageId)` flow supports: * **non-consumable** (buy once + restore): great for “lifetime unlock” * **consumable** (buy many times): great for “credits”, “tokens”, etc Configure this in `.a0/monetization.yaml` (v2) with `pricingTiers[].productType`: ```yaml theme={null} version: 2 bundles: - ref: pro name: Pro # Only required if you're linking existing *subscription* SKUs ios: subscriptionGroupId: "123456789" pricingTiers: # Subscription - ref: pro_monthly type: monthly productType: subscription displayName: Monthly prices: { USD: 9.99 } ios: productId: com.example.pro.monthly # One-time: lifetime unlock (non-consumable) - ref: pro_lifetime type: lifetime productType: non_consumable displayName: Lifetime prices: { USD: 49.99 } ios: productId: com.example.pro.lifetime # One-time: credits pack (consumable) - ref: credits_100 type: lifetime productType: consumable displayName: 100 Credits prices: { USD: 4.99 } ios: productId: com.example.credits.100 ``` Notes: * `type: lifetime` + **no** `productType` defaults to `non_consumable` (backwards compatible). * Consumables are **not restorable** on iOS. If you need purchase history, use `customerInfo.nonSubscriptionTransactions`. * For consumables (credits/tokens), treat the purchase as a transaction and fulfill/grant the balance server-side. Do not use local storage as your source of truth. ## Understanding Offerings Structure Before building your paywall, it's important to understand how offerings are structured: ```javascript theme={null} { all: { "offering-id-1": { identifier: "offering-id-1", availablePackages: [...], monthly: {...}, annual: {...} }, "offering-id-2": { identifier: "offering-id-2", availablePackages: [...] } }, current: { // The offering marked as "current" in your dashboard identifier: "offering-id-1", availablePackages: [...] } } ``` * `offerings.all` is an **object** with offering IDs as keys (not an array) - `offerings.current` contains the offering marked as current in your a0 dashboard - If no offering is set as current, `offerings.current` will be `null` - Use `offerings.current` when possible, or access specific offerings via their ID from `offerings.all` ## packageId vs productId (don’t mix these up) * packageId = pkg.identifier (what you pass to purchase(packageId)) * productId = pkg.product.identifier (the store purchase ID)
* iOS: StoreKit product identifier (SKU)
* Stripe: Stripe price ID
If you're using monetization.yaml v2, each pricing tier has a stable ref (pricingTiers\[].ref). The purchases API includes this as pkg.a0\_packageRef, and purchase() accepts it as an alias (LLM-friendly). ## Building a Paywall Here's a simple paywall component using the `useA0Purchases` hook: ```javascript theme={null} import { useA0Purchases } from "a0-purchases"; function PaywallScreen() { const { isPremium, isLoading, offerings, purchase, restore } = useA0Purchases(); // Get packages from current offering (recommended) or first available offering const currentOffering = offerings?.current || Object.values(offerings?.all || {})[0]; const packages = currentOffering?.availablePackages || []; const handlePurchase = async (pkg) => { const packageId = pkg.identifier; // what purchase() expects const productId = pkg.product.identifier; // Stripe price ID or iOS SKU try { await purchase(packageId); console.log("Purchased", { packageId, productId }); if (isPremium) { // Success! Navigate to premium content } } catch (error) { if (!error.userCancelled) { // Show error to user } } }; if (isLoading) { return ; } if (packages.length === 0) { return No products available; } return ( {packages.map((pkg) => ( handlePurchase(pkg)}> {pkg.product.title} {pkg.product.priceString} {pkg.packageType === "WEEKLY" ? "per week" : pkg.packageType === "MONTHLY" ? "per month" : pkg.packageType === "ANNUAL" ? "per year" : ""} ))}