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
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
| Rule | Constraint |
|---|---|
| Characters | Lowercase letters a-z, digits 0-9, hyphens - |
| Length | 1 to 64 characters |
| Edge hyphens | Cannot start or end with - |
| Uniqueness | Must be globally unique while the publication is active |
| Immutability | Cannot be changed after publication creation |
// 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.
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).
morse publication create --name <name> --slug <slug>
The new publication is automatically selected as the active publication.
morse publication create --name "My Blog" --slug my-blog
Created "My Blog" (0x1a2b...3c4d) ownerCap: 0x5e6f... publisherCap: 0x7a8b... tx: AbCdEf... Selected as the active publication.
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 }
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 → OwnerCapPublisherCap: 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>).
morse publication delete [<slug|id>]
Omit [<slug|id>] to target the active one:
morse publication delete my-blog
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...", });
public fun delete_publication( registry: &mut PublicationRegistry, publication: Publication, owner_cap: OwnerCap, )
On deletion:
- The
OwnerCapis consumed and its UID deleted - The slug is removed from the registry,
SlugReleasedevent is emitted - The revocation denylist is dropped
- The publication UID is deleted
- A
PublicationDeletedevent is emitted
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 deletionPublisherCap 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>).
morse publication transfer-ownership <recipient> [--publication <slug|id>]
morse publication transfer-ownership 0x71a3...9f2b
import { transferOwnership } from "@arcadiasystems/morse-sdk"; await transferOwnership(adapter, config, { ownerCapId: "0x5e6f...", recipient: "0x71a3...9f2b", });
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.
| Event | When emitted | Fields |
|---|---|---|
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.
| Code | Constant | Meaning |
|---|---|---|
6 | ESlugAlreadyExists | A publication with this slug already exists in the registry |
7 | ESlugEmpty | The provided slug is an empty string |
8 | ESlugTooLong | Slug exceeds 64 characters |
9 | ESlugInvalidChar | Slug contains a character that is not a-z, 0-9, or - |
10 | ESlugInvalidEdgeHyphen | Slug starts or ends with a hyphen |
2 | EUnauthorized | Capability does not belong to this publication |
4 | EPublisherCapWrongHolder | Publisher cap is bound to a different address than the transaction sender |
5 | EPublisherCapRevoked | Publisher cap has been revoked by the owner |
0 | ECollectionAlreadyExists | A collection with this name already exists |