# Assets and locations

> A location hierarchy (site, building, unit) and an asset registry with tags, status, coordinates and assignment to members or records. Optional PostGIS stores points and areas as geography; booking resources and work items can name an asset.

Source: https://bettersupabase.com/docs/blocks/assets

`assets` keeps two registries per organization. Locations form a tree, such
as a site with buildings and units inside it, and assets (a laptop, a van, a
meeting room, a machine) sit at a location, carry a tag and a serial number,
and are assigned to a member or to a record such as a party.

Reading needs `assets.read`, creating and changing locations and assets needs
`assets.manage`, and assigning needs `assets.assign`. Admins get all three and
members get `assets.read`. A member also reads the assets assigned to them
without `assets.read`.

```bash
better-supabase add assets
```

The module needs `tenant` and `access`. It works with
[bookings](/docs/blocks/bookings) and [work items](/docs/blocks/work-items)
when they are installed, and records `asset.*` events to the outbox and the
audit log.

```ts
import { createAssets } from "better-supabase/blocks/assets";

const assets = createAssets({ transport, tenant: organizationId });

const site = await assets
  .createLocation({
    kind: "site",
    name: "Amsterdam office",
    code: "AMS",
    point: { latitude: 52.37, longitude: 4.89 },
  })
  .orThrow();

const laptop = await assets
  .createAsset({
    kind: "laptop",
    name: "MacBook Pro 14",
    locationId: site.id,
    tag: "IT-0042",
    serialNumber: "C02XYZ",
  })
  .orThrow();

await assets
  .assign(laptop.id, { type: "member", id: userId }, { note: "New hire" })
  .orThrow();
```

## Locations [#locations]

A location has a free-text `kind` (`site`, `building`, `floor`, `unit`), a
`name`, an optional `code` that is unique in the organization, an `address`
object and an optional `point`. `parentId` places it in the tree: the parent
must belong to the same organization, and a move that would put a location
inside one of its own sublocations fails with `ASSETS_LOCATION_CYCLE`.

`location(id)` returns the location with its `path` from the top, and how
many open sublocations and assets in service it holds. `locations()` lists the
top locations, and `locations({ parentId })` the children of one.
`archiveLocation(id)` archives a location once it has no open sublocations
and no assets in service.

## Assets [#assets]

An asset has a `kind`, a `name`, an optional `locationId`, a `tag` that is
unique in the organization, a `serialNumber`, a `point` and a `status`:
`active`, `maintenance` or `retired`. Retiring an asset ends its assignment,
and a retired asset can't be assigned again.

`assets(filter)` lists assets newest first, filtered by location (with
`includeSublocations` for everything under it), kind, status, assignee or a
search on name, tag and serial number, and pages with `before`. `asset(id)`
adds the location path and the booking resources and open work items that
name the asset.

## Assignment [#assignment]

`assign(assetId, assignee)` assigns an asset to a member
(`{ type: "member", id: userId }`) or to any record (`{ type, id }`), and
ends the previous assignment. A member must be able to read the
organization's assets. `release(assetId)` ends the assignment, and
`assignments(assetId)` lists the history newest first.

`assignedTo(subject)` lists the assets a member or record holds; the
[assets section](/docs/blocks/records) renders them on the record page. The
`before_asset_assign` and `after_asset_assign` hooks run around an
assignment.

## Coordinates and PostGIS [#coordinates-and-postgis]

Locations and assets store `latitude` and `longitude`. `near({ latitude,
longitude, radiusMeters })` returns the assets and locations within the
radius, nearest first, each with `distanceMeters`. Without PostGIS this is a
haversine distance on the stored coordinates.

The `postgis` option installs the `postgis` extension in the `extensions`
schema and adds geography columns: a generated `point` on locations and
assets, and an `area` on locations (a GeoJSON polygon or multipolygon, stored
as a multipolygon). Both get GiST indexes, `near` uses `ST_DWithin`, and
`locationsAt(point)` returns the locations whose area contains a point.
Without the option, `locationsAt` fails with `ASSETS_POSTGIS_REQUIRED`.

```ts
blocks: {
  assets: { options: { postgis: true } },
}
```

## Bookings and work items [#bookings-and-work-items]

With [bookings](/docs/blocks/bookings) installed, booking resources get an
`asset_id` column, so a meeting room or vehicle you book is an asset. With
[work items](/docs/blocks/work-items) installed, work items get one for the
asset a job is about. `link({ type: "booking_resource" | "work_item", id },
assetId)` sets it, and `null` clears it. Locations and assets are registered
subjects, so [relations](/docs/blocks/relations), comments and attachments
work on them.

## Errors [#errors]

| Hint                           | Cause                                                     |
| ------------------------------ | --------------------------------------------------------- |
| `ASSETS_FORBIDDEN`             | the caller lacks the permission for this                  |
| `ASSETS_LOCATION_NOT_FOUND`    | no location with this id in the organization              |
| `ASSETS_NOT_FOUND`             | no asset with this id in the organization                 |
| `ASSETS_CODE_TAKEN`            | a location with this code already exists                  |
| `ASSETS_TAG_TAKEN`             | an asset with this tag already exists                     |
| `ASSETS_LOCATION_CYCLE`        | the parent is one of the location's own sublocations      |
| `ASSETS_LOCATION_IN_USE`       | the location still holds open sublocations or assets      |
| `ASSETS_RETIRED`               | the asset is retired                                      |
| `ASSETS_ASSIGNEE_INVALID`      | the assignee isn't valid, or the member can't read assets |
| `ASSETS_POINT_INVALID`         | a latitude or longitude is missing or out of range        |
| `ASSETS_RADIUS_INVALID`        | the radius isn't a positive number of meters              |
| `ASSETS_PATCH_INVALID`         | the patch names a key that can't be changed               |
| `ASSETS_POSTGIS_REQUIRED`      | `locationsAt` needs the `postgis` option                  |
| `ASSETS_LINK_UNSUPPORTED`      | the link target isn't a booking resource or a work item   |
| `ASSETS_LINK_TARGET_NOT_FOUND` | no booking resource or work item with this id             |

## Events [#events]

`asset_location.created`, `asset_location.updated` and
`asset_location.archived`, and `asset.created`, `asset.updated`,
`asset.assigned` and `asset.released`. The
[events page](/docs/extending/events) lists their payloads.