Developers

Building with Web Tiles

What a tile is on AT, the tools that make, run and publish them, and how to put all of that in your own app.

The model

A tile is a bundle of web resources described by a MASL manifest in bundle mode: a name, a resources map from paths to content-addressed blobs with their HTTP headers (at least content-type), and the usual web-app-manifest extras (description, icons, screenshots, categories, theme_color, background_color, sizing). The / entry is what gets rendered. When it runs, a tile can only load what its manifest lists; the embedding app mediates everything else. The Web Tiles spec has the details, including the CSP that a tile execution context must enforce.

On AT, a tile is a record in the ing.dasl.masl collection of its author's repository. Each resource is uploaded as a blob, and the manifest's src entries are blob refs, so the PDS keeps the blobs alive:

{
  "$type": "ing.dasl.masl",
  "cid": "bafyrei…",            // the DRISL (dag-cbor) CID of "tile"
  "createdAt": "2026-10-02T15:26:26.697Z",
  "tile": {
    "name": "Cosmic Junkyard",
    "description": "Dodge falling dice…",
    "sizing": { "width": 720, "height": 520 },
    "icons": [{ "src": "/icon.png" }],
    "screenshots": [{ "src": "/shot.png" }],
    "prev": { "$link": "bafyrei…" },   // optional: manifest CID of the version this replaces
    "resources": {
      "/": {
        "src": { "$type": "blob", "ref": { "$link": "bafkrei…" }, "mimeType": "application/octet-stream", "size": 50720 },
        "content-type": "text/html; charset=utf-8"
      },
      "/icon.png": { "src": { … }, "content-type": "image/png" }
    }
  }
}

Making tiles

A tile source directory is just index.html (it becomes /), a manifest.json with at least a name, and whatever else the page needs. Content types are guessed from file names; anything you put under resources in the manifest is kept.

atile (@dasl/tiles)

The command-line tool. It bundles a directory into a .tile, or publishes it to your repo with an app password.

npm install -g @dasl/tiles

atile bundle ./my-tile my-tile.tile          # write a carball, no login needed
atile login you.example app-password          # stored in the OS keychain
atile publish ./my-tile                       # upload blobs + write the record (fresh TID)
atile publish -s ./my-tile                    # stable id: reuse this directory's record key next time
atile publish --tid 3mwvoqidyqd24 ./my-tile   # an explicit record key
atile delete at://did:plc:…/ing.dasl.masl/3mwvoqidyqd24

-s remembers the at:// URI per directory in ~/.atile/identifiers.json, so republishing overwrites the same record. atile does not set prev; add "prev": { "$link": "…" } to manifest.json yourself if you want the chain.

Blastile

Blastile is a desktop app (Deno Desktop) for working with tiles: open a .tile or an at:// URI, watch a source directory with live reload (the CAR is rebuilt on every change), and publish over AT OAuth. It shares ~/.atile/identifiers.json with atile, so a directory keeps the same record whichever tool publishes it. It also hosts its own tile server, so tiles run isolated on random *.localhost origins.

tilers

tilers follows RSS, Atom and JSON feeds and turns each new entry into a self-contained tile (article, styles, fonts, images), saved as .tile and optionally published to your repo.

This site

The publish page takes a .tile or a directory, lets you check and edit the manifest, pick the record key (a fresh TID, your own, or an existing tile to update with prev set), previews the tile, and writes it to your repo from the browser.

Running tiles in your app

Tiles must run in an origin of their own with the spec's CSP sandbox, which the browser can't fake in-page. @dasl/tile-loader gives you a mothership on your page, which mints an iframe on a random subdomain of a load domain running @dasl/tile-server; the server serves the shuttle runtime with the right headers, and the shuttle asks the mothership for the tile's resources over postMessage. The tile never touches the network itself.

npm install @dasl/tile-loader

import { TileMothership } from '@dasl/tile-loader';
import { ATTileLoader } from '@dasl/tile-loader/at';

const ship = new TileMothership({ loadDomain: 'load.webtil.es' }); // or your own
ship.init();                      // once per page
ship.addLoader(new ATTileLoader());

const tile = await ship.loadTile('at://did:plc:…/ing.dasl.masl/3mwvoqidyqd24');
document.body.append(await tile.renderCard());        // name, icon, screenshot; runs on click
document.body.append(tile.renderContent(480));        // or run it now, in a 480px-high iframe
tile.manifest.sizing;                                 // the size the tile asked for
npm install @dasl/tile-server

import express from 'express';
import { createTileLoadingRouter } from '@dasl/tile-server';

const app = express();
app.set('trust proxy', 'loopback');
app.use(createTileLoadingRouter('tiles.example'));   // answers load.tiles.example and *.tiles.example
app.listen(1503);

Every tile load is a new hostname, so the certificate has to be a wildcard (DNS-01), not per-host on-demand issuance. Tile-server 2.1 also ships /.well-known/web-tiles/data.js, a tiny module tiles can import to exchange data with the host through the shuttle (listen(), sendData(), addDataHandler()); the host listens for tiles-protocol-up-* messages on window and posts tiles-protocol-down-* ones to the shuttle iframe.

Publishing from your app

There is no publish API in the @dasl packages yet; it is a short sequence of XRPC calls against the author's PDS, which the publish page implements in the browser:

  1. Compute each resource's CID (CIDv1, raw codec 0x55, sha-256; @atcute/cid's create(CODEC_RAW, bytes)). Resources with the same bytes share a CID.
  2. com.atproto.repo.uploadBlob each one with Content-Type: application/octet-stream, and check that the PDS returns the same CID.
  3. Build the manifest with src: { "$type": "blob", "ref": { "$link": cid }, "mimeType": "application/octet-stream", "size": bytes.length } and a content-type per resource; set prev to the previous manifest CID when you are replacing a tile.
  4. cid = the DRISL CID of that tile object (@atcute/cbor's encode, then create(CODEC_DCBOR, bytes)). Encoding { "$link": … } yields CBOR tag 42, as it must.
  5. com.atproto.repo.putRecord with collection: "ing.dasl.masl", your record key, and { $type, cid, tile, createdAt }.

With OAuth, ask for atproto repo:ing.dasl.masl blob:*/*: the repo: scope covers the collection, blob:*/* the uploads. Then POST https://appmosphe.re/api/index with {"actor": did} so the tile shows up here within seconds rather than on the next relay sweep.

Finding tiles