Cloud Anchors
Cloud Anchors - Cross-Platform AR Anchor Sharing
Cloud Anchors enable you to share AR experiences across multiple devices and platforms (iOS and Android). When you host an anchor, it's uploaded to a cloud provider and can be resolved by any device with the cloud anchor ID.
New in v2.53.0 - ReactVision is the default provider
As of v2.53.0, cloud anchors use the ReactVision platform (
"reactvision"provider) by default. You will need anrvApiKeyandrvProjectIdfrom ReactVision Studio — see ReactVision Studio Setup for details. The existinghostCloudAnchor,resolveCloudAnchor, andonCloudAnchorStateChangeAPI is unchanged regardless of provider.To use Google Cloud Anchors instead, explicitly set
provider="arcore"on yourViroARSceneNavigatorand follow the Google Cloud Setup below. The oldcloudAnchorProviderprop is deprecated — useproviderinstead.
Features
- Cross-Platform: Share anchors between iOS and Android devices — iOS host → Android resolve and vice versa is fully supported
- Persistent: Anchors can be stored for 1–365 days via the
ttlDaysparameter - Accurate: Sub-centimeter accuracy in good conditions
- Scalable: Supports multiple concurrent users
- GPS tagging (ReactVision): Set the device location before hosting to embed GPS coordinates as anchor metadata, enabling proximity search via
rvFindNearbyCloudAnchors - Immediate error feedback (ReactVision):
hostCloudAnchorreturns an error immediately if the environment has not been sufficiently observed — no silent fallback
Requirements
Regardless of which provider you choose, the following requirements apply:
- iOS: iOS 12.0+ with ARKit support, ARCore SDK for iOS
- Android: Android 7.0+ (API 24) with ARCore support
- Network: Internet connection required for cloud anchor operations
See the Provider Setup guide for a full comparison of the ReactVision and ARCore providers, and setup instructions for each.
Setup
For full setup instructions for both the ReactVision and ARCore providers, see the Provider Setup guide.
Cloud Anchors require camera and internet permissions. If you are also using GPS tagging (via rvFindNearbyCloudAnchors), you will additionally need location permissions — see the Provider Setup guide for details.
Usage
Basic Example - Hosting
Use ViroARPlaneSelector to let users tap on a detected plane to place and host an anchor:
import React, { useRef, useState } from "react";
import {
ViroARSceneNavigator,
ViroARScene,
ViroARPlaneSelector,
ViroNode,
ViroBox,
} from "@reactvision/react-viro";
function HostScene({ sceneNavigator }) {
const [cloudAnchorId, setCloudAnchorId] = useState<string | null>(null);
const [isHosting, setIsHosting] = useState(false);
const handlePlaneSelected = async (anchor) => {
if (isHosting) return;
setIsHosting(true);
try {
// Host the anchor with TTL of 1 day (API key auth limit)
const result = await sceneNavigator.hostCloudAnchor(anchor.anchorId, 1);
if (result.success && result.cloudAnchorId) {
console.log("Cloud Anchor ID:", result.cloudAnchorId);
setCloudAnchorId(result.cloudAnchorId);
// Share this ID with other users via your backend
} else {
console.error("Failed to host:", result.error);
}
} catch (error) {
console.error("Failed to host anchor:", error);
} finally {
setIsHosting(false);
}
};
return (
<ViroARScene>
<ViroARPlaneSelector onPlaneSelected={handlePlaneSelected}>
{/* This content appears at the anchor location */}
<ViroBox
position={[0, 0.05, 0]}
scale={[0.1, 0.1, 0.1]}
materials={["boxMaterial"]}
/>
</ViroARPlaneSelector>
</ViroARScene>
);
}
export default function App() {
return (
<ViroARSceneNavigator
// provider defaults to "reactvision" — set provider="arcore" to use Google Cloud Anchors instead
initialScene={{ scene: HostScene }}
/>
);
}
Basic Example - Resolving
Resolve a cloud anchor and place content at its position:
function ResolveScene({ sceneNavigator, cloudAnchorIdToResolve }) {
const [resolvedPosition, setResolvedPosition] = useState(null);
useEffect(() => {
const resolve = async () => {
try {
const result = await sceneNavigator.resolveCloudAnchor(
cloudAnchorIdToResolve
);
if (result.success && result.anchor) {
console.log("Resolved at:", result.anchor.position);
setResolvedPosition(result.anchor.position);
} else {
console.error("Failed to resolve:", result.error);
}
} catch (error) {
console.error("Failed to resolve anchor:", error);
}
};
if (cloudAnchorIdToResolve) {
resolve();
}
}, [cloudAnchorIdToResolve]);
return (
<ViroARScene>
{resolvedPosition && (
<ViroNode position={resolvedPosition}>
<ViroBox
position={[0, 0.05, 0]}
scale={[0.1, 0.1, 0.1]}
materials={["boxMaterial"]}
/>
</ViroNode>
)}
</ViroARScene>
);
}
API Reference
ViroARSceneNavigator Props
| Prop | Type | Description |
|---|---|---|
provider | "reactvision" | "arcore" | "none" | Selects the cloud anchor backend. Defaults to "reactvision" (ReactVision platform — no Google Cloud setup needed). Set to "arcore" to use Google Cloud Anchors (requires API key). Set to "none" to disable. |
onCloudAnchorStateChange | (event) => void | Callback fired when cloud anchor state changes. |
sceneNavigator Methods
hostCloudAnchor(anchorId: string, ttlDays?: number): Promise<ViroHostCloudAnchorResult>
hostCloudAnchor(anchorId: string, ttlDays?: number): Promise<ViroHostCloudAnchorResult>Hosts a local anchor to the cloud.
| Parameter | Type | Description |
|---|---|---|
anchorId | string | The local anchor ID (from ViroAnchor.anchorId) |
ttlDays | number | Time-to-live in days (1-365). Default: 1 |
Returns:
{
success: boolean;
cloudAnchorId?: string; // The cloud anchor ID to share
error?: string;
state: ViroCloudAnchorState;
}
resolveCloudAnchor(cloudAnchorId: string): Promise<ViroResolveCloudAnchorResult>
resolveCloudAnchor(cloudAnchorId: string): Promise<ViroResolveCloudAnchorResult>Resolves a cloud anchor by its ID.
| Parameter | Type | Description |
|---|---|---|
cloudAnchorId | string | The cloud anchor ID to resolve |
Returns:
{
success: boolean;
anchor?: {
anchorId: string;
cloudAnchorId: string;
position: [number, number, number];
rotation: [number, number, number];
scale: [number, number, number];
state: ViroCloudAnchorState;
};
node?: ViroARNode; // May be null for cloud anchors
error?: string;
state: ViroCloudAnchorState;
}
Note: The node property may be null for resolved cloud anchors. Use the anchor.position to place content at the resolved location.
cancelCloudAnchorOperations(): void
cancelCloudAnchorOperations(): voidCancels all pending cloud anchor operations.
ReactVision-Only Methods
The following 8 methods are only available when using the ReactVision provider (the default). They are powered by the proprietary ReactVision Cloud Anchors (RVCA) SDK.
| Method | Description |
|---|---|
rvGetCloudAnchor(anchorId) | Fetch a single anchor record |
rvListCloudAnchors(limit, offset) | Paginated list of all project anchors |
rvUpdateCloudAnchor(id, name, desc, isPublic) | Rename / re-describe an anchor |
rvDeleteCloudAnchor(anchorId) | Permanently delete an anchor and its assets |
rvFindNearbyCloudAnchors(lat, lng, radius, limit) | GPS proximity search |
rvAttachAssetToCloudAnchor(id, url, size, name, type, userId) | Attach a hosted file |
rvRemoveAssetFromCloudAnchor(anchorId, assetId) | Remove an attached asset |
rvTrackCloudAnchorResolution(...) | Record resolve analytics manually |
rvGetCloudAnchor(anchorId)
rvGetCloudAnchor(anchorId)Fetch a single cloud anchor record.
const result = await sceneNavigator.rvGetCloudAnchor("cloud-anchor-abc");
if (result.success) {
console.log(result.anchor.name, result.anchor.isPublic);
}
rvListCloudAnchors(limit, offset)
rvListCloudAnchors(limit, offset)Paginated list of all cloud anchors in your project.
const result = await sceneNavigator.rvListCloudAnchors(20, 0);
result.anchors.forEach(a => console.log(a.anchorId, a.name));
rvUpdateCloudAnchor(id, name, desc, isPublic)
rvUpdateCloudAnchor(id, name, desc, isPublic)Rename or re-describe an existing cloud anchor.
| Parameter | Type | Description |
|---|---|---|
id | string | The cloud anchor ID |
name | string | New display name (optional) |
desc | string | New description (optional) |
isPublic | boolean | Whether the anchor is publicly listed (optional) |
await sceneNavigator.rvUpdateCloudAnchor(
"cloud-anchor-abc",
"Coffee Shop Table",
"The corner table near the window",
true
);
rvDeleteCloudAnchor(anchorId)
rvDeleteCloudAnchor(anchorId)Permanently delete a cloud anchor and all its attached assets.
await sceneNavigator.rvDeleteCloudAnchor("cloud-anchor-abc");
rvFindNearbyCloudAnchors(lat, lng, radius, limit)
rvFindNearbyCloudAnchors(lat, lng, radius, limit)GPS proximity search for cloud anchors. Requires GPS tagging to have been set on the anchors during hosting.
| Parameter | Type | Description |
|---|---|---|
lat | number | Latitude in degrees |
lng | number | Longitude in degrees |
radius | number | Search radius in meters |
limit | number | Maximum number of results |
const nearby = await sceneNavigator.rvFindNearbyCloudAnchors(
37.7749, -122.4194, 200, 10
);
nearby.anchors.forEach(a => console.log(a.anchorId, a.name));
rvAttachAssetToCloudAnchor(id, url, size, name, type, userId)
rvAttachAssetToCloudAnchor(id, url, size, name, type, userId)Attach a hosted file to a cloud anchor so it loads automatically when resolved. Assets can be uploaded via rvUploadAsset — see the Asset Uploads documentation.
| Parameter | Type | Description |
|---|---|---|
id | string | The cloud anchor ID |
url | string | URL of the hosted asset |
size | number | File size in bytes |
name | string | Display name for the asset |
type | string | Asset type ("3d-model", "image", etc.) |
userId | string | The app user ID who owns the asset |
const uploadResult = await sceneNavigator.rvUploadAsset(
"3d-model", "decoration.glb", fileData, "user-123"
);
const hostResult = await sceneNavigator.hostCloudAnchor(localAnchorId, 30);
if (uploadResult.success && hostResult.success) {
await sceneNavigator.rvAttachAssetToCloudAnchor(
hostResult.cloudAnchorId,
uploadResult.url,
fileData.length,
"decoration.glb",
"3d-model",
"user-123"
);
}
rvRemoveAssetFromCloudAnchor(anchorId, assetId)
rvRemoveAssetFromCloudAnchor(anchorId, assetId)Remove a previously attached asset from a cloud anchor.
await sceneNavigator.rvRemoveAssetFromCloudAnchor(
"cloud-anchor-abc",
"asset-xyz-789"
);
rvTrackCloudAnchorResolution(...)
rvTrackCloudAnchorResolution(...)Record resolve analytics manually. Useful for tracking how often and where your cloud anchors are being resolved.
await sceneNavigator.rvTrackCloudAnchorResolution(
cloudAnchorId,
resolveTimeMs,
success
);
Cloud Anchor States
type ViroCloudAnchorState =
| "None"
| "Success"
| "ErrorInternal"
| "TaskInProgress"
| "ErrorNotAuthorized" // Invalid or missing API key
| "ErrorResourceExhausted" // Quota exceeded
| "ErrorHostingDatasetProcessingFailed" // Need more visual data
| "ErrorCloudIdNotFound" // Cloud anchor expired or doesn't exist
| "ErrorResolvingSdkVersionTooOld"
| "ErrorResolvingSdkVersionTooNew"
| "ErrorHostingServiceUnavailable";
Best Practices
1. Ensure Good Tracking Quality
Cloud anchors work best when:
- The environment has distinct visual features
- Lighting is consistent
- The device has moved around the anchor location
- At least 30 seconds of tracking data is available
// Wait for good tracking before hosting
const handleAnchorFound = async (anchor: ViroAnchor) => {
// Give ARKit/ARCore time to build a good map
await new Promise((resolve) => setTimeout(resolve, 3000));
const result = await sceneNavigator.hostCloudAnchor(anchor.anchorId, 7);
// ...
};
2. Handle Errors Gracefully
const result = await sceneNavigator.hostCloudAnchor(anchorId, 7);
switch (result.state) {
case "Success":
// Success!
break;
case "ErrorNotAuthorized":
console.error("Check your API key configuration");
break;
case "ErrorResourceExhausted":
console.error("API quota exceeded. Try again later.");
break;
case "ErrorHostingDatasetProcessingFailed":
console.error("Move the device around to capture more environment data");
break;
default:
console.error("Error:", result.error);
}
3. Use Appropriate TTL
| Use Case | Recommended TTL |
|---|---|
| Real-time multiplayer | 1 day |
| Event-based experience | 1-7 days |
| Persistent installation | 365 days |
Important: API key authentication only supports TTL of 1 day. For TTL > 1 day, you need to configure keyless authorization using OAuth or Service Accounts. See ARCore Cloud Anchors Authorization for details.
4. Share Cloud Anchor IDs
Cloud anchor IDs need to be shared between devices. Common approaches:
- Firebase Realtime Database: Real-time sync for multiplayer
- QR Codes: Simple sharing for co-located experiences
- REST API: Your own backend for custom logic
- Nearby Connections: Peer-to-peer sharing
Quotas and Limits
| Resource | Limit |
|---|---|
| Host requests | 30 per minute per project |
| Resolve requests | 300 per minute per project |
| Anchor TTL | 1-365 days |
| Cloud anchor size | ~50KB per anchor |
Troubleshooting
"ErrorNotAuthorized"
- Verify your API key is correct
- Ensure ARCore API is enabled in Google Cloud Console
- Check that the API key is properly configured for your bundle ID / package name
"ErrorHostingDatasetProcessingFailed"
- Move the device around more to capture environment data
- Ensure good lighting conditions
- Try hosting in an area with more visual features
"ErrorCloudIdNotFound"
- The cloud anchor may have expired (exceeded TTL)
- The cloud anchor ID may be incorrect
- Network connectivity issues
iOS: "GARSession initialization failed"
- Verify
GARAPIKeyis in Info.plist - Ensure ARCore/CloudAnchors pod is installed
- Check Xcode build settings (Bitcode = NO)
Android: Cloud anchors not working
- Verify API key in AndroidManifest.xml
- Ensure device supports ARCore
- Check that ARCore app is installed and updated
Example: Multiplayer AR Game
import { useEffect, useState } from "react";
import { database } from "./firebase"; // Your Firebase setup
function MultiplayerARScene({ sceneNavigator, roomId }) {
const [sharedObjects, setSharedObjects] = useState([]);
// Listen for cloud anchors shared by other players
useEffect(() => {
const unsubscribe = database
.ref(`rooms/${roomId}/anchors`)
.on("child_added", async (snapshot) => {
const { cloudAnchorId, objectType } = snapshot.val();
const result = await sceneNavigator.resolveCloudAnchor(cloudAnchorId);
if (result.success && result.anchor) {
setSharedObjects((prev) => [
...prev,
{
id: snapshot.key,
position: result.anchor.position,
objectType,
},
]);
}
});
return () => unsubscribe();
}, [roomId]);
// Host a new object and share with other players
const placeObject = async (anchor, objectType) => {
const result = await sceneNavigator.hostCloudAnchor(anchor.anchorId, 1);
if (result.success) {
// Share with other players via Firebase
await database.ref(`rooms/${roomId}/anchors`).push({
cloudAnchorId: result.cloudAnchorId,
objectType,
timestamp: Date.now(),
});
}
};
return (
<ViroARScene onAnchorFound={(anchor) => placeObject(anchor, "cube")}>
{sharedObjects.map((obj) => (
<ViroNode key={obj.id} position={obj.position}>
<ViroBox scale={[0.1, 0.1, 0.1]} />
</ViroNode>
))}
</ViroARScene>
);
}
Updated 17 days ago