What’s new since you were here

Developer docs

Add Atmosphere “Open in…” links, smart client recommendations, compose intents, and AT-URI resolution to your own app, with two MIT-licensed packages and a public API.

@aturi.to/waypointson npmx ↗
View raw .md →

Building with an LLM or coding agent? Grab the whole page as Markdown.

Overview

The same waypoint catalog, recommendations, and link logic that power aturi.to are published as two standalone, MIT-licensed npm packages so you can drop them into any Atmosphere (AT Protocol) app:

  • @aturi.to/waypoints, a zero-dependency, framework-agnostic core: the client catalog, per-client “Open in…” link builders, recommendations, and URL ⇄ AT-URI resolution. Works in the browser, Node 18+, and edge runtimes.
  • @aturi.to/waypoints-react: a headless-first React picker UI plus client icons, built on the core. Ships zero CSS by default and is fully themeable.

Both are dual-licensed MIT (the aturi.to app itself is GPL-3.0) to remove the adoption barrier. Prefer not to install anything? The hosted Resolve API does the same work over HTTP.

@aturi.to/waypoints

The zero-dependency core. Turn an AT URI into per-client links, recommend the best client for a record type, and reverse-resolve a pasted URL back into an AT URI.

bash
npm install @aturi.to/waypoints

Resolve an AT URI or a pasted URL

ts
import { resolveAtUri, resolveUrl } from '@aturi.to/waypoints';

// AT URI -> waypoints
const result = resolveAtUri('at://did:plc:abc/app.bsky.feed.post/3k7');
result?.waypoints;   // [{ id: 'anisota', name: 'Anisota', category, url }, ...]
result?.recommended; // { ids: ['bluesky', 'anisota', ...], label: 'Recommended for Posts' }

// Pasted page URL -> waypoints (offline pattern match)
const fromUrl = await resolveUrl('https://bsky.app/profile/alice.bsky.social/post/3k7');

What’s included

  • High-level resolvers: resolveAtUri, resolveUrl, buildWaypointsForParsed, and resolveViaApi (a typed client for the hosted endpoint).
  • Catalog & recommendations: getWaypointDataForType, getCategorizedWaypointsData, getRecommendedWaypointsData, and the raw WAYPOINT_DESTINATIONS_DATA catalog.
  • Parsing: parseURI, parseAtUri, matchSupportedUrl, resolveHandle.
  • Capabilities: supportsComposeIntent, getComposeIntentUrl, getComposeIntentWaypoints — see compose intents.

A handful of destinations (pdsls, atp.tools, Margin, Grain, Popfeed) only produce useful URLs when a DID is known; they’re filtered out unless a DID is available, so pass one in or supply a resolveHandle to resolveUrl. Full reference in the package README.

@aturi.to/waypoints-react

A drop-in React “Open in…” picker. Headless-first: it ships zero CSS and emits stable, namespaced styling hooks, so you can use your own design system, opt into the polished theme, or drop down to a hook and render everything yourself. It re-exports the entire core, so a single install gives you the components and the resolvers.

bash
npm install @aturi.to/waypoints-react
# peers (you almost certainly already have react/react-dom):
npm install react react-dom lucide-react

1. Drop-in picker

Renders clean semantic markup with no CSS attached. Every element carries a data-aturi-wp attribute and an aturi-wp-* class; map your own via classNames, pass unstyled to drop the built-ins, or replace rows with the renderWaypoint prop.

tsx
import { WaypointPicker } from '@aturi.to/waypoints-react';

<WaypointPicker
  type="post"
  handle="alice.bsky.social"
  collection="app.bsky.feed.post"
  rkey="3k7qw..."
/>;

2. The useWaypoints hook

For full control, the hook returns render-ready data plus copy / open helpers: no markup at all.

tsx
import { useWaypoints } from '@aturi.to/waypoints-react';

function MyPicker() {
  const { recommended, categories, waypoints, copy, open } = useWaypoints({
    type: 'post',
    handle: 'alice.bsky.social',
    collection: 'app.bsky.feed.post',
    rkey: '3k7qw...',
  });

  return (
    <ul>
      {waypoints.map((w) => (
        <li key={w.id}>
          {w.icon}
          <button onClick={() => open(w.url)}>{w.name}</button>
          <button onClick={() => copy(w.url)}>Copy</button>
        </li>
      ))}
    </ul>
  );
}

3. The polished theme (opt-in)

Want the Aturi look without writing CSS? Import the stylesheet once. It targets the namespaced classes and is fully themeable via --aturi-wp-* CSS custom properties (with light/dark defaults).

tsx
import '@aturi.to/waypoints-react/styles.css';
import { WaypointPicker } from '@aturi.to/waypoints-react';

Server vs. client: the package is a client component (it carries "use client"), so it works out of the box in the Next.js App Router. For framework-agnostic helpers inside a Server Component, import them from @aturi.to/waypoints directly.

Resolve API

Don’t want to install anything? Hit the hosted endpoint from a share sheet, an Apple Shortcut, or any client: no login, no API keys. It returns the resolved waypoints and recommendations for a page URL or an AT URI.

http
GET https://aturi.to/api/resolve?url=<encoded-page-url>
GET https://aturi.to/api/resolve?atUri=at://...

The core package’s resolveViaApi() is a typed client for this endpoint. It’s the right choice from a browser, where fetching arbitrary pages is blocked by CORS.

To ask about the catalog itself rather than a specific record — what’s in it, and which clients can do what — there’s a companion endpoint: GET /api/waypoints, filterable by ?type= and ?capability=.

Compose intents

bsky.app can be handed a link that opens its composer pre-filled: /intent/compose?text=… (see the intent link docs). Clients forked from the official social app inherit the same route, so the catalog records which ones do — and every waypoint carries a composeIntent, null when the client has no confirmed route.

ts
import {
  WAYPOINT_DESTINATIONS_DATA,
  getComposeIntentUrl,
  getComposeIntentWaypoints,
  supportsComposeIntent,
} from '@aturi.to/waypoints';

// Which clients will open a composer for you?
getComposeIntentWaypoints().map((w) => w.id);
// ['anisota', 'bluesky', 'impro', 'blacksky', 'witchsky', 'mu', 'deer', 'northsky']

supportsComposeIntent(WAYPOINT_DESTINATIONS_DATA.deer); // true
getComposeIntentUrl(WAYPOINT_DESTINATIONS_DATA.deer, 'hello from my app');
// 'https://deer.social/intent/compose?text=hello%20from%20my%20app'

Two nuances worth reading off the data rather than assuming. prefillsText is false for a client that routes the intent but ignores the text, so a “share this” link would open an empty composer — fine as a jump, useless as a share. And appUrl appears only where the client publishes a native scheme, so it’s a bonus, not a fallback.

Over HTTP, the same data comes back on both endpoints. Pass the text to get finished links, or take urlTemplate and substitute the URL-encoded text for {text} yourself.

http
GET https://aturi.to/api/waypoints?capability=compose
GET https://aturi.to/api/waypoints?capability=compose&text=<encoded-text>

# or inline, on any resolve call:
GET https://aturi.to/api/resolve?atUri=at://...&composeText=<encoded-text>
json
{
  "id": "deer",
  "name": "Deer",
  "category": "blueskyForks",
  "composeIntent": {
    "url": "https://deer.social/intent/compose?text=hello",
    "urlTemplate": "https://deer.social/intent/compose?text={text}",
    "textParam": "text",
    "prefillsText": true
  }
}

In React, each useWaypoints entry carries the same composeIntent; pass composeText to the hook to have the links built for you.

License

@aturi.to/waypoints and @aturi.to/waypoints-react are MIT © atpotato, LLC. The aturi.to web app and browser extension are licensed GPL-3.0; the packages are intentionally dual-licensed MIT so other Atmosphere developers can adopt them freely.

Building an Atmosphere client and want it in the catalog? See the supported waypoints or open a PR.

General
Navigation
Current page

Keyboard shortcuts

Press ? any time to open this list.

General

  • Command paletteCtrl+Kor/
  • Keyboard shortcuts?
  • Toggle themeT

Navigation

  • Go to homeGthenH
  • Go to explorerGthenE
  • Go to universal linksGthenL
  • Go to browser extensionGthenX
  • Go to docsGthenD
  • Go to settingsGthenS

Current page

  • Copy page linkNot set