How it works Examples Docs Pricing Blog WP Blocks Install free

Blog /

The bridge cookbook: ten patterns you’ll reach for

The bridge API reference is the right place to land when you know what you’re looking for. It lists every method, every error code, every permission scope, in alphabetical order, with the precision that a reference doc demands.

The problem with reference docs is they don’t work as onboarding. A new developer scaffolding their first DSGo App doesn’t know dsgo.posts.list exists yet. They know they want to “show recent posts.” Reference docs answer the wrong question for that user.

This post is the patterns half. Ten recipes that cover most of what people build, each with the smallest possible manifest delta and the smallest possible code snippet. If you copy one of these into a starter and it works, you can read the reference for the second one and the docs will make more sense, because you’ll have a concrete example to map onto.

Each recipe assumes you’ve scaffolded with npx designsetgo apps init and you have the bridge client imported:

import { dsgo } from '@designsetgo/app-client';
await dsgo.ready;

The await dsgo.ready line is non-negotiable. It resolves once the parent has acknowledged the handshake; bridge calls before it are an error.

1. List the most recent posts

"permissions": { "read": ["posts"], "write": [] }
const { items } = await dsgo.posts.list({ per_page: 5 });
for (const post of items) {
  console.log(post.title, post.date);
}

The shape of items is documented in the reference. The fields you’ll usually want: id, title (rendered string, plain), date, excerpt (rendered string, plain), link. The bridge flattens the WP REST {rendered, raw} envelope into bare strings; if you’ve worked with the raw /wp-json/wp/v2/posts endpoint, the bridge shape is simpler.

2. Greet the current user (or fall back gracefully)

"permissions": { "read": ["user"], "write": [] }
const user = await dsgo.user.current();
const name = user ? user.name : 'guest';
greeting.textContent = `Hi, ${name}.`;

dsgo.user.current() resolves either to a { id, name, slug, email, avatar_url, roles } object for a logged-in visitor or to null for an anonymous one. Branch on truthiness; no try/catch needed. This is the right pattern for “personalize if I know you, fall back if I don’t.”

3. Capability-gate a button

"permissions": { "read": ["user"], "write": [] }
const canEdit = await dsgo.user.can('edit_posts');
editButton.disabled = !canEdit;

dsgo.user.can runs the same capability check WordPress would run for the rest of wp-admin. You don’t have to roll your own role logic; if the user is an editor, the call returns true; if their role got demoted, it returns false. Cache the result if you’ll check it more than once on the same render.

4. Persist app-scoped state across sessions

"permissions": { "read": [], "write": [] }
const settings = (await dsgo.storage.app.get('settings')) ?? { theme: 'light' };
settings.theme = 'dark';
await dsgo.storage.app.set('settings', settings);

App storage is per-app, shared across all visitors of the site. Use it for things every user of the app should see: configuration the admin set, content the app authored, leaderboard tallies. Storage doesn’t activate a permission bucket and doesn’t appear in the install dialog as a row. (We talked about why in the permission buckets post: adding a row trains users to ignore the rest, and storage is universal enough not to need its own.)

5. Persist per-user state

"permissions": { "read": ["user"], "write": [] }
const prefs = (await dsgo.storage.user.get('prefs')) ?? { dismissed_tour: false };
prefs.dismissed_tour = true;
await dsgo.storage.user.set('prefs', prefs);

User storage is keyed by the visitor’s user ID. Logged-out visitors get a session-keyed bucket that does not survive logout. The bridge handles all the keying for you; your app sees raw values, never the keys. This is the right pattern for “remember this visitor’s preferences” without rolling a per-user database table.

6. Render full post HTML with WordPress block styles

"permissions": { "read": ["posts"], "write": [] },
"content": { "blockStyles": ["core", "auto"] }
const post = await dsgo.posts.get(123);
postContainer.innerHTML = post.content;
dsgo.content.applyBlockStyles(post);

Without applyBlockStyles, your innerHTML is a structural skeleton: paragraphs, headings, images, but no styling. The plugin attaches a content_styles payload to each post response (when your manifest declares the content block), and applyBlockStyles pulls the stylesheets into your iframe, scoped to your container, so the rendered post looks like it would on the site. The call is idempotent and deduplicated.

7. Route-aware page rendering

For mount.mode: "root" apps, the visitor’s URL path is part of the bridge context:

await dsgo.ready;
const path = dsgo.context.path;

if (path === '/about/') {
  renderAbout();
} else if (path.startsWith('/posts/')) {
  const slug = path.split('/').filter(Boolean).pop();
  renderPostDetail(slug);
}

dsgo.router.subscribe((loc) => {
  routeChanged(loc.path);
});

dsgo.context.path is a property, not a method (no parens, no await — read it after dsgo.ready resolves). Combine it with dsgo.router.subscribe(handler) if your app handles client-side routing and wants to react to URL changes without a full reload. dsgo.router.navigate(path, opts?) is the navigation half; calling it updates the URL bar and triggers any subscribed handlers.

8. Call the site’s configured AI Connector

"permissions": { "read": ["ai"], "write": [] },
"ai": { "max_tool_calls": 5 }
const response = await dsgo.ai.prompt({
  messages: [
    { role: 'user', content: 'Summarize this paragraph in one sentence.' },
  ],
});
console.log(response.content);

The site’s configured Connector handles the actual provider call. Your app never sees an API key, never pays for inference, and stays portable across whichever provider the site is on. The response shape is { content, usage: { input_tokens, output_tokens }, tool_calls }. If the site has no Connector configured, the call rejects with ai_not_configured; design your fallback so the rest of the page still renders without the AI piece. The architectural rationale is in WP 7.0 changed what a plugin can be.

9. Publish an ability the site’s agent can call

"isolation": "iframe",
"abilities": {
  "publishes": [
    {
      "name": "my-app/word-count",
      "label": "Count words",
      "description": "Returns the word count of provided text.",
      "category": "content",
      "input_schema": {
        "type": "object",
        "properties": { "text": { "type": "string" } },
        "required": ["text"]
      },
      "output_schema": {
        "type": "object",
        "properties": { "count": { "type": "integer" } }
      },
      "annotations": { "readonly": true, "destructive": false, "idempotent": true }
    }
  ]
}
dsgo.abilities.implement('my-app/word-count', async ({ text }) => {
  return { count: text.trim().split(/\s+/).filter(Boolean).length };
});

Once installed, my-app/word-count is visible to the site’s AI agent like any other Abilities API ability. Your app gets called automatically when the agent decides this is the tool it needs. Iframe-mode only in v1; the publisher mounts a hidden iframe of your app on demand to handle the call. See Apps as Abilities for the broader architecture, and Apps as Abilities deep dive for the vertical-pack pattern.

10. Drive a detail page from a WordPress dataset

This one needs both a manifest route and a render-time bridge call. In the manifest:

{
  "path": "/posts/:slug",
  "file": "post/index.html",
  "dataset": { "source": "wp:posts", "id_field": "slug" }
}

In the rendered template (your single post/index.html):

await dsgo.ready;
const { slug } = dsgo.context.routeParams;
const { items } = await dsgo.posts.list({ slug, per_page: 1 });
const post = items[0];

document.querySelector('h1').textContent = post.title;
document.querySelector('article').innerHTML = post.content;

dsgo.context.routeParams is a property (no parens). dsgo.posts.get(id) takes only a numeric ID in v1; to fetch by slug, query posts.list with the slug filter and take the first match.

One HTML file, every post on the site. Built-in dataset sources include wp:posts, wp:pages, wp:cpt:<slug>, and wc:products. Custom resolvers register through a WP filter; that’s covered in Dynamic routes deep dive.

What’s deliberately not here

A few things you might expect that v1 doesn’t ship:

  • fetch to arbitrary origins. Each app declares an explicit runtime.external_origins allowlist (full HTTPS origins, no wildcards). The CSP enforces it. If your app needs to talk to an external service, it goes through a declared, reviewable path. AI calls go through dsgo.ai.prompt(), not raw fetch.
  • Write methods. permissions.write exists in the manifest schema but stays empty in v1. Write access is a different trust conversation, both at the bridge level (what does the app’s permission disclosure say?) and at the runtime level (how do we audit what changed?). v1 leaned conservative; v2 will widen the surface.
  • Direct DOM access to the parent page. The bridge is the entire surface. The iframe sandbox prevents direct DOM access by design. If you find yourself wanting to reach into the parent’s DOM, you’re either fighting the runtime or solving the problem the wrong way.
  • A “re-render the whole bridge” reset. Bridge state is per-iframe-mount. If you need to reset, navigate the iframe to a new URL inside your bundle, or unmount and remount the parent’s iframe.

How to use this list

A useful pattern: when you’re about to write a feature, scan this post for the closest match. If one of these recipes is 80% of what you want, copy it as the starting point and modify. Most DSGo App features are compositions of two or three of these patterns. “List recent posts, gate the edit button by capability, persist the visitor’s preferred sort order” is recipes 1 + 3 + 5 in three lines of glue.

If none of these recipes is close to what you want, two questions are worth asking. First: does the bridge actually expose what you need? Check the reference. Second: is the right answer client-side bridge calls at all, or should the data be substituted server-side via a dataset route (recipe 10)? Server-side substitution is faster and SEO-friendlier; bridge calls are more flexible.

Where to go next

  • BRIDGE-API.md for the full reference, every method, every error code, every permission scope.
  • MANIFEST.md for the manifest schema.
  • The next several posts in this calendar walk through specific use cases (contact form, member portal, recipe ranker) that compose these patterns into real apps.