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 an rvApiKey and rvProjectId from ReactVision Studio — see ReactVision Studio Setup for details. The existing hostCloudAnchor, resolveCloudAnchor, and onCloudAnchorStateChange API is unchanged regardless of provider.

To use Google Cloud Anchors instead, explicitly set provider="arcore" on your ViroARSceneNavigator and follow the Google Cloud Setup below. The old cloudAnchorProvider prop is deprecated — use provider instead.

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 ttlDays parameter
  • 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): hostCloudAnchor returns 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

PropTypeDescription
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) => voidCallback fired when cloud anchor state changes.

sceneNavigator Methods

hostCloudAnchor(anchorId: string, ttlDays?: number): Promise<ViroHostCloudAnchorResult>

Hosts a local anchor to the cloud.

ParameterTypeDescription
anchorIdstringThe local anchor ID (from ViroAnchor.anchorId)
ttlDaysnumberTime-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>

Resolves a cloud anchor by its ID.

ParameterTypeDescription
cloudAnchorIdstringThe 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

Cancels 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.

MethodDescription
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)

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)

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)

Rename or re-describe an existing cloud anchor.

ParameterTypeDescription
idstringThe cloud anchor ID
namestringNew display name (optional)
descstringNew description (optional)
isPublicbooleanWhether the anchor is publicly listed (optional)
await sceneNavigator.rvUpdateCloudAnchor(
  "cloud-anchor-abc",
  "Coffee Shop Table",
  "The corner table near the window",
  true
);
rvDeleteCloudAnchor(anchorId)

Permanently delete a cloud anchor and all its attached assets.

await sceneNavigator.rvDeleteCloudAnchor("cloud-anchor-abc");
rvFindNearbyCloudAnchors(lat, lng, radius, limit)

GPS proximity search for cloud anchors. Requires GPS tagging to have been set on the anchors during hosting.

ParameterTypeDescription
latnumberLatitude in degrees
lngnumberLongitude in degrees
radiusnumberSearch radius in meters
limitnumberMaximum 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)

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.

ParameterTypeDescription
idstringThe cloud anchor ID
urlstringURL of the hosted asset
sizenumberFile size in bytes
namestringDisplay name for the asset
typestringAsset type ("3d-model", "image", etc.)
userIdstringThe 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)

Remove a previously attached asset from a cloud anchor.

await sceneNavigator.rvRemoveAssetFromCloudAnchor(
  "cloud-anchor-abc",
  "asset-xyz-789"
);
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 CaseRecommended TTL
Real-time multiplayer1 day
Event-based experience1-7 days
Persistent installation365 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

ResourceLimit
Host requests30 per minute per project
Resolve requests300 per minute per project
Anchor TTL1-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 GARAPIKey is 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>
  );
}