morseDOCS

Publications

A publication is the top-level permission management and identity boundary for a content domain in Morse. Everything (collections, entries, permissions) lives inside a publication.

What is a publication?

A Publication is a shared Sui object that acts as the root container for one content brand or site. It bundles:

  • A human-readable name (display label)
  • An immutable, globally unique slug (canonical URL key)
  • A collection map holding all named content lanes
  • A revocation denylist for publisher capabilities
Move
public struct Publication has key {
  id: UID,
  name: String,
  slug: String,
  collections: VecMap<String, Collection>,
  revoked_publisher_caps: Table<ID, bool>,
}

The Publication type has key ability but no store. Sharing and transfer are gated through this module's own functions, enforced by the Sui type system. You cannot wrap or freely transfer a publication; you must call share_publication.

The slug system

Every publication is identified by a slug: a short, lowercase string that becomes its canonical identifier across the protocol. Slugs are enforced to be globally unique by the PublicationRegistry.

Slug rules

RuleConstraint
CharactersLowercase letters a-z, digits 0-9, hyphens -
Length1 to 64 characters
Edge hyphensCannot start or end with -
UniquenessMust be globally unique while the publication is active
ImmutabilityCannot be changed after publication creation
Examples
// valid slugs
"my-blog"
"acme-corp"
"docs2025"

// invalid slugs
"-leading-hyphen"    // ESlugInvalidEdgeHyphen
"trailing-hyphen-"   // ESlugInvalidEdgeHyphen
"UPPERCASE"          // ESlugInvalidChar
"spaces not allowed" // ESlugInvalidChar
""                   // ESlugEmpty

Slug reuse after deletion

When a publication is deleted, its slug is released from the registry and becomes available again. This is a deliberate tradeoff: it gives publishers flexibility to re-register slugs, but teams should be aware that a released slug can be re-claimed by anyone.

PublicationRegistry

The PublicationRegistry is a shared singleton object created once in the package init function. It maintains a global Table<String, ID> mapping slugs to publication IDs, enforcing uniqueness across all publications.

Move
public struct PublicationRegistry has key {
  id: UID,
  slugs: Table<String, ID>,
}

// Look up a slug
public fun contains_slug(registry: &PublicationRegistry, slug: String): bool

// Resolve slug → publication ID
public fun get_publication_id_by_slug(registry: &PublicationRegistry, slug: String): &ID

The registry is a mandatory argument to new_publication. You cannot create a publication without passing the shared registry object.

To list publications owned by your account, use the CLI: morse publication list. You can also fetch a specific publication by slug with morse publication get <slug>.

See CLI Commands → Publication for the full syntax.

Creating a publication

Publication creation follows the pure-function pattern: new_publication returns objects without performing any transfers. The caller decides what to do with them in a Programmable Transaction Block (PTB).

bash
morse publication create --name <name> --slug <slug>

The new publication is automatically selected as the active publication.

example
morse publication create --name "My Blog" --slug my-blog
output
Created "My Blog" (0x1a2b...3c4d)
  ownerCap:     0x5e6f...
  publisherCap: 0x7a8b...
  tx:           AbCdEf...
Selected as the active publication.
typescript
import { createPublication } from "@arcadiasystems/morse-sdk";

// See sdk-client-setup.html for adapter and config initialization
const pub = await createPublication(adapter, config, {
  name: "My Blog",
  slug: "my-blog",
});
// pub → { publicationId, ownerCapId, publisherCapId, digest, gasUsedMist }
Move
public fun new_publication(
  registry: &mut PublicationRegistry,
  name: String,
  slug: String,
  ctx: &mut TxContext,
): (Publication, OwnerCap, PublisherCap)

// Share the publication so other transactions can access it
public fun share_publication(publication: Publication)

// Transfer caps using the contract's typed wrappers (key-only, no store)
public fun transfer_owner_cap(owner_cap: OwnerCap, recipient: address)
public fun transfer_publisher_cap(cap: PublisherCap, recipient: address)

Use SDK Setup → morseConfig() to get the package ID and registry ID, SDK Setup → Server setup to initialize the client, and Contract Cookbooks for complete PTB examples.

new_publication returns three objects in a tuple:

  • Publication: the shared content container
  • OwnerCap: admin authority over the publication (one per publication, held by the owner wallet). See Access Control → OwnerCap
  • PublisherCap: write authority for content operations, bound to the transaction sender. See Access Control → PublisherCap

Deleting a publication

Publications can be deleted, but only when all collections have been removed first. Attempting to delete a publication with remaining collections aborts with an error.

All commands target the active publication. Empty every collection first (entries, then the collection itself), then delete the publication. Pass -y before the subcommand to skip the prompt (e.g. morse -y publication delete <slug>).

bash
morse publication delete [<slug|id>]

Omit [<slug|id>] to target the active one:

example
morse publication delete my-blog
typescript
import { deletePublication } from "@arcadiasystems/morse-sdk";

// reader verifies all collections are empty before submitting the tx
await deletePublication(reader, adapter, config, {
  publicationId: "0x1a2b...3c4d",
  ownerCapId: "0x5e6f...",
});
Move
public fun delete_publication(
  registry: &mut PublicationRegistry,
  publication: Publication,
  owner_cap: OwnerCap,
)

On deletion:

  1. The OwnerCap is consumed and its UID deleted
  2. The slug is removed from the registry, SlugReleased event is emitted
  3. The revocation denylist is dropped
  4. The publication UID is deleted
  5. A PublicationDeleted event is emitted
Collections must be empty before deletion
The VecMap of collections will abort on destroy_empty if any collections remain. Remove all collections (and their entries) before calling delete_publication.
PublisherCaps are orphaned on deletion
Deleting a publication does not destroy outstanding PublisherCap objects. Any caps issued to other addresses remain as owned Sui objects, but they become permanently unusable. The publication they reference no longer exists, so all write operations will fail. Revoke or destroy caps before deleting the publication if you want a clean teardown.

Ownership transfer

Publication ownership can be transferred by transferring the OwnerCap to a new address. Since OwnerCap has only the key ability, transfer is handled by the module:

Targets the active publication. Pass --publication <slug> to target a different one. Pass -y before the subcommand to skip the prompt (e.g. morse -y publication transfer-ownership <recipient>).

bash
morse publication transfer-ownership <recipient> [--publication <slug|id>]
example
morse publication transfer-ownership 0x71a3...9f2b
typescript
import { transferOwnership } from "@arcadiasystems/morse-sdk";

await transferOwnership(adapter, config, {
  ownerCapId: "0x5e6f...",
  recipient: "0x71a3...9f2b",
});
Move
public fun transfer_owner_cap(owner_cap: OwnerCap, recipient: address)

The recipient immediately gains full admin authority: the ability to issue/revoke publisher caps and delete the publication. Existing publisher caps remain valid until individually revoked.

After receiving the OwnerCap, the recipient must set the publication as active in their CLI before they can manage it: morse use <slug>.

Publisher cap management

Publishers are managed through capability tokens. An owner can issue, revoke, and destroy publisher caps. Revoked caps are added to the publication's denylist, and any future operation attempted with a revoked cap aborts with EPublisherCapRevoked.

See Access Control → PublisherCap for the full API with Move, CLI, and SDK examples.

Events

The publications module emits the following events. An off-chain indexer can subscribe to these to maintain a live view of the publication registry.

EventWhen emittedFields
PublicationCreated New publication created publication: ID, name: String, slug: String
PublicationDeleted Publication deleted publication: ID, name: String
SlugRegistered Slug claimed in registry slug: String, publication: ID
SlugReleased Slug freed on deletion slug: String, publication: ID
PublisherCapIssued New publisher cap minted publication: ID, cap: ID
PublisherCapRevoked Publisher cap revoked by owner publication: ID, cap: ID

Error codes

For more error codes, see: Collections, Entries, Access Control.

CodeConstantMeaning
6ESlugAlreadyExistsA publication with this slug already exists in the registry
7ESlugEmptyThe provided slug is an empty string
8ESlugTooLongSlug exceeds 64 characters
9ESlugInvalidCharSlug contains a character that is not a-z, 0-9, or -
10ESlugInvalidEdgeHyphenSlug starts or ends with a hyphen
2EUnauthorizedCapability does not belong to this publication
4EPublisherCapWrongHolderPublisher cap is bound to a different address than the transaction sender
5EPublisherCapRevokedPublisher cap has been revoked by the owner
0ECollectionAlreadyExistsA collection with this name already exists