# orderedItems[].object.content
Anatomy of an E2EE ActivityPub Note in Enigmatick

Over the break I spent some time revamping my structs and enums for handling #E2EE #ActivityPub exchanges. This is what I have at the moment for the initial Create from the client (this is subject to change).

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://purl.archive.org/socialweb/mls"
  ],
  "type": "Create",
  "actor": "https://enigmatick.social/user/jdt",
  "to": [
    "https://enigmatick.social/user/mls-tester"
  ],
  "cc": [],
  "object": {
    "mediaType": "message/mls",
    "encoding": "base64",
    "published": "2025-12-29T21:11:17Z",
    "type": "Note",
    "to": [
      "https://enigmatick.social/user/mls-tester"
    ],
    "cc": [],
    "attributedTo": "https://enigmatick.social/user/jdt",
    "content": "AAEAAhDIkTdahK5U3s1SQBbA2fIvAAAAAAAAAAEBABzHJj3+WZDE3Hw6H5W9YNhE3bhDSyExL4tnn0E5QOvPUeOgYdo/2n+rGUI1M8wupeK2bt919jAs8yTj2awvBAsprxAInHm8qWgq2FER2xjcpAx9f42ncAL8Xehfy3v6vonGuAPkYteAzdFCC8mGWxg3slpI+VoPrSWThSKqjMNxttZSfvbnhEFYw0jUFmJwxOecDS8n3UVjuLDt14V0Fw9YMv+XXUgKj9bWaCPi0qhuatcgxCqXwXzpevJBhY0trbPZgjB8LlhC/zcx5NNLI9WJtCF9zq3RPkG6ByPdH6IWTH5drOJyvefILElybP4eGMSoGWZATmGUQm/k3iKG6tRyzMM/fHxpIz34",
    "tag": [
      {
        "type": "Mention",
        "name": "[at]mls-tester@enigmatick.social",
        "href": "https://enigmatick.social/user/mls-tester"
      }
    ],
    "attachment": []
  },
  "published": "2025-12-29T21:11:17Z",
  "instrument": [
    {
      "type": "VaultItem",
      "content": "oAjN0c2+q5gCtrFlauGU1kbKGizm5a5pH+iY5cObdihVBjjsSiSbkeu6XQ7/RR4lcn0OIqS9O+MVJ0QyympRxt7JI9qzCGlkCFcAyp4oe8nhh/YG7WHoLWL272vPqlq8A/Yf0YFprhIBlU48sJF6A1iRlU4fEBaRwnwV4gniSCQr55jpDe++M/HD3bSFJNcvpmf5BH9G4Z3VBaB8HMnDECGzFrB8zFycAR0w1SNYbGJuXFsopfMb4EfPkZoHT28="
    },
    {
      "type": "Welcome",
      "mediaType": "message/mls",
      "encoding": "base64",
      "content": "AAFAmCDd32lw8FF8QC6uFjAHr3mgvxBPjuGV1Pssxx+h5CNhcCAv4/tP+dWUbCq6L4A+aMr+L1uP3Xkusb4ea2H80EgFOUBUqHDCdGzjAhZdLCrjE0kM/aou36ZSZc0ahftKmE07chTiMUo8FgIj12deS4A0l3sXAR8x724HeigtyObI1GK7fxMGXU5oWemHGhS1unxK6XVyiuqvQriKP+VJPZa6JzN3Yb4r4l9B5jUfNUc5gX3XvzkzcO57Exj8SgpTTygo2zBTOPQ+6steYycUA70ed3LPKEfSCllIWeWAV+J+EFh7ywm90XFYzaPrXVudIx3orRzVMFnIldLax3oHUW5uXIrGQ06JWfgdOQ6DByb2iy/Dw4zJlbZP1oJptBTgO19IqUQaz58wrWDBSiYIT3h1jOXE3GvJgeeQIiW78noO5MF3xI3WRXB4C0cttyKm7Od2CBOk1USo+TDLCevnmExhlKlVTobTMMI4xZ1vVCdYpnb0k9rO7coVKPueBqVg2mE20GS/IoRG0v8+h0xUKIgsbYua41krjXoB5hctX4kH3lRO+qERkoXEcLVxRJuyeZEGsk4XrJ18AigU1a8gB6cZZ5LyvcV756yelu6F6MRQKba6zawDLOn4Takm3Lr3urfWo5Br7DBZcigMVjzGn8MhLSIEMhv6Ehmqajj9Vl6sTnpi90tVYWUtKO1buPrk3AiXrsbnEoNubqCs1sVmA93/Z0CcE8GQa1dO7NywofSV4/ax/tmBCZEoU6/uL7ygZHt8ZvSyd6wniPCR6sfd4ys4O8C7qW1KKFtEBbb1lMre494ZeXI5v0sG7MwbwFtI3d2uRHtWZ8v9khGr1ds4AFz3QnvoMSUtsR2kvq+C6H45kUs2ae957+txpKt5MDrcmK+CTTcX/FhlAtib4mlbs+Aj4WJFLOkXuB+MlHOMBlMz3ndJ5I3GDbo+VQvdJC2mCUOBWraihOV92mQQHi2gLXAA1NSsZIjXgnyVSYsrUvNJgTvw+AOVmJ7q5tXENPlZoOKUjSB3fEmKlywf90EQLUjDpphnYtvdFz8Oh8Vbgze+QAQp2VVYImI+1voBErK1EMKqEDFz1DC1CWUzqhl3k2pMgx00JhP3ZZpbX7YtjRmLjJM="
    }
  ]
}

The mention of mls-tester has an [at] to avoid triggering my automatic webfinger resolution.

I've adopted the mediaType and encoding attributes from the draft ActivityPub specification. And I've standardized the Welcome message to incorporate those.

The Create includes the Welcome and a VaultItem as instruments rather than being sent as separate activities. The Welcome can include any number of recipients, so one within the activity should be sufficient.

The VaultItem is specific to the #Enigmatick client and isn't intended to be federated. That's how I'm handling ongoing access to the decrypted data; the VaultItem is a symmetrically encrypted representation of the original content that can be safely stored on the server for repeated use by the client. I'll need to eventually update the @context with that extension.

# orderedItems[].object.source.content
## Anatomy of an E2EE ActivityPub Note in Enigmatick

Over the break I spent some time revamping my `structs` and `enums` for handling #E2EE #ActivityPub exchanges. This is what I have at the moment for the initial `Create` from the client (this is subject to change).

```json
{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://purl.archive.org/socialweb/mls"
  ],
  "type": "Create",
  "actor": "https://enigmatick.social/user/jdt",
  "to": [
    "https://enigmatick.social/user/mls-tester"
  ],
  "cc": [],
  "object": {
    "mediaType": "message/mls",
    "encoding": "base64",
    "published": "2025-12-29T21:11:17Z",
    "type": "Note",
    "to": [
      "https://enigmatick.social/user/mls-tester"
    ],
    "cc": [],
    "attributedTo": "https://enigmatick.social/user/jdt",
    "content": "AAEAAhDIkTdahK5U3s1SQBbA2fIvAAAAAAAAAAEBABzHJj3+WZDE3Hw6H5W9YNhE3bhDSyExL4tnn0E5QOvPUeOgYdo/2n+rGUI1M8wupeK2bt919jAs8yTj2awvBAsprxAInHm8qWgq2FER2xjcpAx9f42ncAL8Xehfy3v6vonGuAPkYteAzdFCC8mGWxg3slpI+VoPrSWThSKqjMNxttZSfvbnhEFYw0jUFmJwxOecDS8n3UVjuLDt14V0Fw9YMv+XXUgKj9bWaCPi0qhuatcgxCqXwXzpevJBhY0trbPZgjB8LlhC/zcx5NNLI9WJtCF9zq3RPkG6ByPdH6IWTH5drOJyvefILElybP4eGMSoGWZATmGUQm/k3iKG6tRyzMM/fHxpIz34",
    "tag": [
      {
        "type": "Mention",
        "name": "[at]mls-tester@enigmatick.social",
        "href": "https://enigmatick.social/user/mls-tester"
      }
    ],
    "attachment": []
  },
  "published": "2025-12-29T21:11:17Z",
  "instrument": [
    {
      "type": "VaultItem",
      "content": "oAjN0c2+q5gCtrFlauGU1kbKGizm5a5pH+iY5cObdihVBjjsSiSbkeu6XQ7/RR4lcn0OIqS9O+MVJ0QyympRxt7JI9qzCGlkCFcAyp4oe8nhh/YG7WHoLWL272vPqlq8A/Yf0YFprhIBlU48sJF6A1iRlU4fEBaRwnwV4gniSCQr55jpDe++M/HD3bSFJNcvpmf5BH9G4Z3VBaB8HMnDECGzFrB8zFycAR0w1SNYbGJuXFsopfMb4EfPkZoHT28="
    },
    {
      "type": "Welcome",
      "mediaType": "message/mls",
      "encoding": "base64",
      "content": "AAFAmCDd32lw8FF8QC6uFjAHr3mgvxBPjuGV1Pssxx+h5CNhcCAv4/tP+dWUbCq6L4A+aMr+L1uP3Xkusb4ea2H80EgFOUBUqHDCdGzjAhZdLCrjE0kM/aou36ZSZc0ahftKmE07chTiMUo8FgIj12deS4A0l3sXAR8x724HeigtyObI1GK7fxMGXU5oWemHGhS1unxK6XVyiuqvQriKP+VJPZa6JzN3Yb4r4l9B5jUfNUc5gX3XvzkzcO57Exj8SgpTTygo2zBTOPQ+6steYycUA70ed3LPKEfSCllIWeWAV+J+EFh7ywm90XFYzaPrXVudIx3orRzVMFnIldLax3oHUW5uXIrGQ06JWfgdOQ6DByb2iy/Dw4zJlbZP1oJptBTgO19IqUQaz58wrWDBSiYIT3h1jOXE3GvJgeeQIiW78noO5MF3xI3WRXB4C0cttyKm7Od2CBOk1USo+TDLCevnmExhlKlVTobTMMI4xZ1vVCdYpnb0k9rO7coVKPueBqVg2mE20GS/IoRG0v8+h0xUKIgsbYua41krjXoB5hctX4kH3lRO+qERkoXEcLVxRJuyeZEGsk4XrJ18AigU1a8gB6cZZ5LyvcV756yelu6F6MRQKba6zawDLOn4Takm3Lr3urfWo5Br7DBZcigMVjzGn8MhLSIEMhv6Ehmqajj9Vl6sTnpi90tVYWUtKO1buPrk3AiXrsbnEoNubqCs1sVmA93/Z0CcE8GQa1dO7NywofSV4/ax/tmBCZEoU6/uL7ygZHt8ZvSyd6wniPCR6sfd4ys4O8C7qW1KKFtEBbb1lMre494ZeXI5v0sG7MwbwFtI3d2uRHtWZ8v9khGr1ds4AFz3QnvoMSUtsR2kvq+C6H45kUs2ae957+txpKt5MDrcmK+CTTcX/FhlAtib4mlbs+Aj4WJFLOkXuB+MlHOMBlMz3ndJ5I3GDbo+VQvdJC2mCUOBWraihOV92mQQHi2gLXAA1NSsZIjXgnyVSYsrUvNJgTvw+AOVmJ7q5tXENPlZoOKUjSB3fEmKlywf90EQLUjDpphnYtvdFz8Oh8Vbgze+QAQp2VVYImI+1voBErK1EMKqEDFz1DC1CWUzqhl3k2pMgx00JhP3ZZpbX7YtjRmLjJM="
    }
  ]
}
```

_The mention of `mls-tester` has an `[at]` to avoid triggering my automatic webfinger resolution._

I've adopted the `mediaType` and `encoding` attributes from the draft ActivityPub specification. And I've standardized the `Welcome` message to incorporate those.

The `Create` includes the `Welcome` and a `VaultItem` as `instruments` rather than being sent as separate activities. The `Welcome` can include any number of recipients, so one within the activity should be sufficient.

The `VaultItem` is specific to the #Enigmatick client and isn't intended to be federated. That's how I'm handling ongoing access to the decrypted data; the `VaultItem` is a symmetrically encrypted representation of the original content that can be safely stored on the server for repeated use by the client. I'll need to eventually update the `@context` with that extension.

# orderedItems[].object.content
Adjusting #Enigmatick's previously built functions to align with the #ActivityPub #E2EE draft.

KeyPackage collection on the Actor object

Request

> curl -H "Accept: application/activity+json" https://enigmatick.social/user/jdt | jq '.keyPackages'

Response

"https://enigmatick.social/user/jdt/keys"

Retrieve the KeyPackage Collection

These are populated by the client (the browser using the wasm module loaded in the Svelte SPA). I had a different mechanism in place for managing this (using the instrument field on Activities) but am updating it to use the Add flow for the Collection as specified by the draft.

Request

> curl -H "Accept: application/activity+json" https://enigmatick.social/user/jdt/keys | jq

Response

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Collection",
  "id": "https://enigmatick.social/user/jdt/keys",
  "totalItems": 39,
  "next": "https://enigmatick.social/user/jdt/keys?mkp=true"
}

Follow the next link to retrieve a KeyPackage

This will mark the KeyPackage as distributed and would typically link it to the Actor that signed the request (that's temporarily disabled for testing).

Request

> curl -H "Accept: application/activity+json" https://enigmatick.social/user/jdt/keys?mkp=true | jq

Response

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://purl.archive.org/socialweb/mls"
  ],
  "type": "Collection",
  "totalItems": 1,
  "items": [
    {
      "type": "KeyPackage",
      "id": "https://enigmatick.social/key-package/cfde7b54-8d08-41e9-8eb5-d997a12e9347",
      "attributedTo": "https://enigmatick.social/user/jdt",
      "to": [
        "https://www.w3.org/ns/activitystreams#Public"
      ],
      "mediaType": "message/mls",
      "encoding": "base64",
      "content": "AAEAASCOWe7uxA8tPeI01nDP8ka2irEhgfkK2qM81/o+tEKPEiCz2h2Vv6VVuGSx/C0WINQCuazZm4rQAymxd1JdHGVvaSDurWzIE3z5eXUFz5coFGv9db7DxZDx0arb/gyRuidCAgABImh0dHBzOi8vZW5pZ21hdGljay5zb2NpYWwvdXNlci9qZHQCAAEIAAEAAgADAE0KAAEAAgADAAQABQwAAQACAAMABAAFAAcCAAEBAAAAAGeDNwsAAAAAZ/IDGwBAQJUffd/8/Efgt0ITBeMWfJYcb7S8E5U7yyUINiIUfqmsGwcvTyam6C+9F+k64NBEs0rXjGMjPA6AgiZ2oR4/1wwAQECliseeQfQb0zgq755ZiiujRqLkEWt8WOTJSsBXHK6hey6bNs4wet782NL8k92h1BNuPZDzxHaHXdXe1wgBO6QJ"
    }
  ]
}

# orderedItems[].object.source.content
Adjusting #Enigmatick's previously built functions to align with the #ActivityPub #E2EE draft.

### KeyPackage collection on the Actor object

Request
```bash
> curl -H "Accept: application/activity+json" https://enigmatick.social/user/jdt | jq '.keyPackages'
```

Response
```json
"https://enigmatick.social/user/jdt/keys"
```

### Retrieve the KeyPackage Collection

These are populated by the client (the browser using the `wasm` module loaded in the Svelte SPA). I had a different mechanism in place for managing this (using the `instrument` field on Activities) but am updating it to use the `Add` flow for the Collection as specified by the draft.

Request
```bash
> curl -H "Accept: application/activity+json" https://enigmatick.social/user/jdt/keys | jq
```

Response
```json
{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Collection",
  "id": "https://enigmatick.social/user/jdt/keys",
  "totalItems": 39,
  "next": "https://enigmatick.social/user/jdt/keys?mkp=true"
}
```

### Follow the `next` link to retrieve a KeyPackage

This will mark the `KeyPackage` as distributed and would typically link it to the Actor that signed the request (that's temporarily disabled for testing).

Request
```bash
> curl -H "Accept: application/activity+json" https://enigmatick.social/user/jdt/keys?mkp=true | jq
```

Response
```json
{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://purl.archive.org/socialweb/mls"
  ],
  "type": "Collection",
  "totalItems": 1,
  "items": [
    {
      "type": "KeyPackage",
      "id": "https://enigmatick.social/key-package/cfde7b54-8d08-41e9-8eb5-d997a12e9347",
      "attributedTo": "https://enigmatick.social/user/jdt",
      "to": [
        "https://www.w3.org/ns/activitystreams#Public"
      ],
      "mediaType": "message/mls",
      "encoding": "base64",
      "content": "AAEAASCOWe7uxA8tPeI01nDP8ka2irEhgfkK2qM81/o+tEKPEiCz2h2Vv6VVuGSx/C0WINQCuazZm4rQAymxd1JdHGVvaSDurWzIE3z5eXUFz5coFGv9db7DxZDx0arb/gyRuidCAgABImh0dHBzOi8vZW5pZ21hdGljay5zb2NpYWwvdXNlci9qZHQCAAEIAAEAAgADAE0KAAEAAgADAAQABQwAAQACAAMABAAFAAcCAAEBAAAAAGeDNwsAAAAAZ/IDGwBAQJUffd/8/Efgt0ITBeMWfJYcb7S8E5U7yyUINiIUfqmsGwcvTyam6C+9F+k64NBEs0rXjGMjPA6AgiZ2oR4/1wwAQECliseeQfQb0zgq755ZiiujRqLkEWt8WOTJSsBXHK6hey6bNs4wet782NL8k92h1BNuPZDzxHaHXdXe1wgBO6QJ"
    }
  ]
}
```

# orderedItems[].object.content
FEP-8fcf implemented last night in #Enigmatick.

[2025-12-18T16:24:56Z DEBUG enigmatick::server::extractors] AxumSigned: pool acquire took 436.968µs
[2025-12-18T16:24:56Z DEBUG enigmatick::server::extractors] Request[target: post /inbox, host: enigmatick.social, signature: keyId="https://[redacted]main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type collection-synchronization",signature="G...s=, content-type: application/activity+json, content-length: 427, user-agent: http.rb/5.1.1 (Mastodon/4.2.10; +[redacted])]
[2025-12-18T16:24:56Z DEBUG enigmatick::server::extractors] AxumSigned: verify_axum took 5.040791ms
[2025-12-18T16:24:56Z DEBUG enigmatick::server::extractors] AxumSigned: update_instance took 26.262904ms
[2025-12-18T16:24:56Z DEBUG enigmatick::server::extractors] AxumSigned: total time 32.021645ms
[2025-12-18T16:24:56Z DEBUG enigmatick::blocklist] checking [redacted] against BlockList
[2025-12-18T16:24:56Z DEBUG enigmatick::blocklist] checking actor [redacted] against BlockList
[2025-12-18T16:24:56Z DEBUG enigmatick::server::routes::inbox] Processing Collection-Synchronization header: collectionId=[redacted]/followers, digest=195650b06cb44278929fd1c2320831f17a2899312dc482de7fc41301778d210a
[2025-12-18T16:24:56Z DEBUG enigmatick::server::routes::inbox::announce] Announce[by: [redacted], obj: ref:[redacted], at: 2025-12-18T16:24:55Z, id: [redacted]/activity]
[2025-12-18T16:24:56Z DEBUG enigmatick::server::follower_sync] Follower sync digest matches for remote actor [redacted], no reconciliation needed

This also fixes some signature verification issues I hadn't noticed were occurring due to the collection-synchronization header not being included in my verification code. I see now how to make that more flexible to accommodate any headers and will be improving that further.

# orderedItems[].object.content
@AdamStuartSmith I had that problem with vanilla Mastodon for a bit when I first installed it. Any posts that got any sort of activity from anyone with a significant following would overwhelm my server for several minutes.

I solved that by setting up a #Varnish cache in front of my server. I don't use Mastodon anymore, but I still use the cache in front of my bespoke software (#Enigmatick). It works really well to take the load off of the core server for those fan-out posts. Varnish handles the load with aplomb.

# orderedItems[].object.source.content
@AdamStuartSmith@sauropods.win I had that problem with vanilla Mastodon for a bit when I first installed it. Any posts that got any sort of activity from anyone with a significant following would overwhelm my server for several minutes.

I solved that by setting up a #Varnish cache in front of my server. I don't use Mastodon anymore, but I still use the cache in front of my bespoke software (#Enigmatick). It works really well to take the load off of the core server for those fan-out posts. Varnish handles the load with aplomb.

# orderedItems[].object.summary
From its conception, #Enigmatick has leaned heavily on the `inbox` and `outbox` endpoints for client operations. [...]

# orderedItems[].object.content
From its conception, #Enigmatick has leaned heavily on the /inbox and /outbox endpoints for client operations. There are some /api endpoints, but I avoid that were I can shoehorn operations into the #ActivityPub specification and #ActivityStreams vocabulary.

While typical operational activities are fairly well accounted for, administration is a weak point. For example: I haven't identified a clear way to use the currently described mechanisms for an administrative user to pull up and manage instances or actors on a server.

I've relied on CLI tools (e.g., ./enigmatick --help) to manage some of that. And in some cases, I know how to manipulate data in my database, so I haven't worried too much about building tooling. But I'd like to ship something that other folks can use to share in my efforts, so I've been thinking about how to model those activities in an ActivityPub-esque way to use in the Svelte UI.

ActivityPub Messages

To that end, I'm now using Block and Delete activities sent from the client to the server outbox to manage the blocking of instances and purging of data.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "ek": "https://enigmatick.social/ns#",
      "Instance": "ek:Instance"
    }
  ],
  "id": "https://enigmatick.social/activities/550e8400-e29b-41d4-a716-446655440000",
  "type": "Block",
  "actor": "https://enigmatick.social/user/system",
  "object": {
    "type": "Instance",
    "id": "https://spammy-instance.example"
  }
}

In practice, my client does not generate the id, but that attribute is generated by the server and the Activity is stored alongside other typically federated activities. These local Block activities are not federated out to other servers; they are intended solely for local server management.

The Block activity is sent as a message signed at the client by a user with administrative privileges on the server. Enigmatick's user authentication is unique (i.e., I use a separate set of encryption keys for client-signing executed by a wasm module in the browser). That can be a topic for a future article.

That the actor is defined as the system Application user is important. That's used by the server to establish the scope of this action as system-wide, not just for a single user. The system actor is discoverable in the nodeinfo metadata.

I'm using a typed object rather than just an id reference. This is so that I can use this same flow for blocking and purging Actor objects (i.e., the type would be Person, Service, or Application).

The purge action is similar, using the Delete activity.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "ek": "https://enigmatick.social/ns#",
      "Instance": "ek:Instance"
    }
  ],
  "id": "https://enigmatick.social/activities/550e8400-e29b-41d4-a716-446655440000",
  "type": "Delete",
  "actor": "https://enigmatick.social/user/system",
  "object": {
    "type": "Instance",
    "id": "https://spammy-instance.example"
  }
}

The term, "delete" is a bit of a misnomer in this case as it applies to the instance specifically. The instance will remain, but the objects, activities, and actors associated with that instance will be fully deleted (i.e., not set to Tombstone).

Collection Endpoints

To facilitate the UI operations, I've created two new collection endpoints on my server: /instances and /actors. These endpoints provide typical ActivityPub Collection objects.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "Instance": "ek:Instance",
      "activitiesCount": "ek:activitiesCount",
      "actorsCount": "ek:actorsCount",
      "blocked": "ek:blocked",
      "ek": "https://enigmatick.social/ns#",
      "lastMessageAt": "ek:lastMessageAt",
      "objectsCount": "ek:objectsCount"
    }
  ],
  "type": "OrderedCollection",
  "id": "https://enigmatick.social/instances",
  "totalItems": 7702,
  "orderedItems": [
    {
      "type": "Instance",
      "id": "https://example-instance.name",
      "blocked": false,
      "created": "2025-12-16T16:56:33Z",
      "lastMessageAt": "2025-12-16T16:56:33Z",
      "actorsCount": 0,
      "objectsCount": 1,
      "activitiesCount": 0
    }
  ],
  "first": "https://enigmatick.social/instances?max=9223372036854775807",
  "last": "https://enigmatick.social/instances?min=0",
  "next": "https://enigmatick.social/instances?max=1765657395402834"
}

I've added some extensions in the @context to account for a few non-standard attributes.

That collection is used by the UI.

Collection Discovery

nodeinfo is a common protocol used for discovering information about ActivityPub-speaking servers. I've extended my use of that to facilitate client-discovery of these new endpoints using the metadata object contained in the nodeinfo JSON.

"metadata": {
  "actor": "https://enigmatick.social/user/system",
  "adminActors": "https://enigmatick.social/actors",
  "adminInstances": "https://enigmatick.social/instances",
  "domain": "enigmatick.social",
  "url": "https://enigmatick.social"
}

Final Thoughts

As I'm reading through this, I see some opportunities for refinement. I should probably be using OrderedCollectionPage instead of OrderedCollection for my collection endpoints. I'm sure there are other tweaks to be made.

# orderedItems[].object.source.content
From its conception, #Enigmatick has leaned heavily on the `/inbox` and `/outbox` endpoints for client operations. There are some `/api` endpoints, but I avoid that were I can shoehorn operations into the #ActivityPub specification and #ActivityStreams vocabulary.

While typical operational activities are fairly well accounted for, administration is a weak point. For example: I haven't identified a clear way to use the currently described mechanisms for an administrative user to pull up and manage instances or actors on a server.

I've relied on CLI tools (e.g., `./enigmatick --help`) to manage some of that. And in some cases, I know how to manipulate data in my database, so I haven't worried too much about building tooling. But I'd like to _ship_ something that other folks can use to share in my efforts, so I've been thinking about how to model those activities in an ActivityPub-esque way to use in the Svelte UI.

## ActivityPub Messages

To that end, I'm now using `Block` and `Delete` activities sent from the client to the server `outbox` to manage the blocking of instances and purging of data.

```json
{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "ek": "https://enigmatick.social/ns#",
      "Instance": "ek:Instance"
    }
  ],
  "id": "https://enigmatick.social/activities/550e8400-e29b-41d4-a716-446655440000",
  "type": "Block",
  "actor": "https://enigmatick.social/user/system",
  "object": {
    "type": "Instance",
    "id": "https://spammy-instance.example"
  }
}
```

In practice, my client does not generate the `id`, but that attribute is generated by the server and the `Activity` is stored alongside other typically federated activities. These local `Block` activities are not federated out to other servers; they are intended solely for local server management.

The `Block` activity is sent as a message signed at the client by a user with administrative privileges on the server. Enigmatick's user authentication is unique (i.e., I use a separate set of encryption keys for client-signing executed by a `wasm` module in the browser). That can be a topic for a future article.

That the `actor` is defined as the system `Application` user is important. That's used by the server to establish the scope of this action as system-wide, not just for a single user. The system actor is discoverable in the `nodeinfo` metadata.

I'm using a typed object rather than just an `id` reference. This is so that I can use this same flow for blocking and purging `Actor` objects (i.e., the `type` would be `Person`, `Service`, or `Application`).

The purge action is similar, using the `Delete` activity.

```json
{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "ek": "https://enigmatick.social/ns#",
      "Instance": "ek:Instance"
    }
  ],
  "id": "https://enigmatick.social/activities/550e8400-e29b-41d4-a716-446655440000",
  "type": "Delete",
  "actor": "https://enigmatick.social/user/system",
  "object": {
    "type": "Instance",
    "id": "https://spammy-instance.example"
  }
}
```
The term, "delete" is a bit of a misnomer in this case as it applies to the instance specifically. The instance will remain, but the `objects`, `activities`, and `actors` associated with that instance will be fully deleted (i.e., not set to `Tombstone`).

## Collection Endpoints

To facilitate the UI operations, I've created two new collection endpoints on my server: `/instances` and `/actors`. These endpoints provide typical ActivityPub `Collection` objects.

```json
{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "Instance": "ek:Instance",
      "activitiesCount": "ek:activitiesCount",
      "actorsCount": "ek:actorsCount",
      "blocked": "ek:blocked",
      "ek": "https://enigmatick.social/ns#",
      "lastMessageAt": "ek:lastMessageAt",
      "objectsCount": "ek:objectsCount"
    }
  ],
  "type": "OrderedCollection",
  "id": "https://enigmatick.social/instances",
  "totalItems": 7702,
  "orderedItems": [
    {
      "type": "Instance",
      "id": "https://example-instance.name",
      "blocked": false,
      "created": "2025-12-16T16:56:33Z",
      "lastMessageAt": "2025-12-16T16:56:33Z",
      "actorsCount": 0,
      "objectsCount": 1,
      "activitiesCount": 0
    }
  ],
  "first": "https://enigmatick.social/instances?max=9223372036854775807",
  "last": "https://enigmatick.social/instances?min=0",
  "next": "https://enigmatick.social/instances?max=1765657395402834"
}
```
I've added some extensions in the `@context` to account for a few non-standard attributes.

That collection is used by the UI.

![The Enigmatick instances UI showing the most recently discovered instances from the enigmatick.social server](https://enigmatick.social/media/uploads/3c327280-2358-468a-bea9-b77d9f016ebb.png)

## Collection Discovery

`nodeinfo` is a common protocol used for discovering information about ActivityPub-speaking servers. I've extended my use of that to facilitate client-discovery of these new endpoints using the `metadata` object contained in the `nodeinfo` JSON.

```json
"metadata": {
  "actor": "https://enigmatick.social/user/system",
  "adminActors": "https://enigmatick.social/actors",
  "adminInstances": "https://enigmatick.social/instances",
  "domain": "enigmatick.social",
  "url": "https://enigmatick.social"
}
```
## Final Thoughts

As I'm reading through this, I see some opportunities for refinement. I should probably be using `OrderedCollectionPage` instead of `OrderedCollection` for my collection endpoints. I'm sure there are other tweaks to be made.

# orderedItems[].object.attachment[].name
The Enigmatick instances UI showing the most recently discovered instances from the enigmatick.social server

# orderedItems[].object.content
@reiver …and it's not just the sheer amount of storage; prudent expiration and cleanup (e.g., #Enigmatick clears cache data each night that's greater than 30 days old) helps to manage that. It's the application load of pulling up the same records tens of thousands of times within a few seconds when new content is created (i.e., the fan-out of initial delivery and the resulting requests back for content).

I use Varnish and that helps a lot with the latter case. But a #FediCDN could be built on Varnish (like Fastly is) and provide decentralization in that service.

# orderedItems[].object.source.content
@reiver@mastodon.social ...and it's not just the sheer amount of storage; prudent expiration and cleanup (e.g., #Enigmatick clears cache data each night that's greater than 30 days old) helps to manage that. It's the application load of pulling up the same records tens of thousands of times within a few seconds when new content is created (i.e., the fan-out of initial delivery and the resulting requests back for content).

I use Varnish and that helps a lot with the latter case. But a #FediCDN could be built on Varnish (like Fastly is) and provide decentralization in that service.

# orderedItems[].object.content
I finally got around to setting up #nodeinfo properly on #Enigmatick as an effort to be a more compliant #ActivityPub participant. Coincidentally (and unexpectedly), my connected instances have jumped substantially (>15% in 24 hours):

enigmatick=> SELECT                                                                                                          CASE                                                                                                                       WHEN created_at > NOW() - INTERVAL '1 day' THEN 'last 24h'                                                               WHEN created_at > NOW() - INTERVAL '2 days' THEN '24-48h ago'                                                            WHEN created_at > NOW() - INTERVAL '3 days' THEN '48-72h ago'                                                          END as period,                                                                                                           COUNT(*)                                                                                                               FROM instances                                                                                                           WHERE created_at > NOW() - INTERVAL '3 days'                                                                             GROUP BY period                                                                                                          ORDER BY period;
   period   | count
------------+-------
 24-48h ago |     1
 48-72h ago |     3
 last 24h   |   164
(3 rows)

Maybe it's not directly related - I also fixed some direct object and tags link references to provide proper ActivityPub representations.

# orderedItems[].object.source.content
I finally got around to setting up #nodeinfo properly on #Enigmatick as an effort to be a more compliant #ActivityPub participant. Coincidentally (and unexpectedly), my connected instances have jumped substantially (>15% in 24 hours):

```sql
enigmatick=> SELECT                                                                                                          CASE                                                                                                                       WHEN created_at > NOW() - INTERVAL '1 day' THEN 'last 24h'                                                               WHEN created_at > NOW() - INTERVAL '2 days' THEN '24-48h ago'                                                            WHEN created_at > NOW() - INTERVAL '3 days' THEN '48-72h ago'                                                          END as period,                                                                                                           COUNT(*)                                                                                                               FROM instances                                                                                                           WHERE created_at > NOW() - INTERVAL '3 days'                                                                             GROUP BY period                                                                                                          ORDER BY period;
   period   | count
------------+-------
 24-48h ago |     1
 48-72h ago |     3
 last 24h   |   164
(3 rows)
```

Maybe it's not directly related - I also fixed some direct object and tags link references to provide proper ActivityPub representations.

# orderedItems[].object.content
I added the ability to manage property values on users' profiles to #Enigmatick. As I'm using this more as my full-time #Fediverse platform, I'm running into things that I've sort of blown off in the past as silly or unnecessary but finding useful now.

In this case, I implemented it using Add and Remove messages posted by the client (a single-page application in Svelte mediated by a wasm module) to the user's outbox on the server. The Actor is the target and the PropertyValue is the Object.

It's not canonically how those #ActivityPub messages should be used (e.g., managing a Collection - the attachment field on an Actor is not really a Collection). But I think logically it makes sense. And it's client communication only; those activities are not federated. When a profile is updated via the Add or Remove, a separate Update is sent out to known servers to update the profile.

Also, I may have gone a little overboard with my own properties.

# orderedItems[].object.source.content
I added the ability to manage property values on users' profiles to #Enigmatick. As I'm using this more as my full-time #Fediverse platform, I'm running into things that I've sort of blown off in the past as silly or unnecessary but finding useful now.

In this case, I implemented it using `Add` and `Remove` messages posted by the client (a single-page application in Svelte mediated by a `wasm` module) to the user's `outbox` on the server. The `Actor` is the target and the `PropertyValue` is the `Object`.

It's not canonically how those #ActivityPub messages should be used (e.g., managing a `Collection` - the `attachment` field on an `Actor` is not really a `Collection`). But I think logically it makes sense. And it's client communication only; those activities are not federated. When a profile is updated via the `Add` or `Remove`, a separate `Update` is sent out to known servers to update the profile.

Also, I may have gone a little overboard with my own properties.

# orderedItems[].object.content
Occasionally a comment to a post is deleted. Or sometimes it hasn't reached my server for whatever reason. In this case, I can tell based on the next comment that it's from Bluesky and I really just don't care enough about that service to figure out why my server may not be pulling it down correctly.

But it sucks to have broken threads because one bit goes missing. So I added in this "Unavailable Comment" component just now.

I think that will help.

#Enigmatick #Threading

# orderedItems[].object.content
Because #Enigmatick is primarily built for me, there are a lot of niceties that I haven't gotten around to adding because "I can just look in the database." I also intentionally don't have any sort of notification mechanism (I may yield on that at some point). I don't even have a field for "email" in my database Actor model.

One of those was a popover UI for Likes and Reposts. I had included the data about who liked and reposted a post months ago in the JSON that the server provides to the client, but I hadn't followed through with utilizing it beyond a basic count.

This is much better than querying my database.

# orderedItems[].object.content
I recently added direct TLS certificate management in #Enigmatick using tls-alpn-01, so all that's needed to get TLS up and running is a public IP address and a domain name pointed at that address. This is managed by a separate reverse proxy process that's bundled in the main Enigmatick binary.

I think while I'm at #DEFCON, I'm going to experiment with Arti (or anything similar) to build a Tor reverse proxy as well. I can't do automatic TLS certificate provisioning for onion services from #LetsEncrypt, but that may be okay.

# orderedItems[].object.content
Late night progress. #LateNightDevs #ActivityPub #Enigmatick

# orderedItems[].object.content
The changes I've made to #Enigmatick over the last couple of months are significant and I plan to write more about them soon. But one of my favorite is this small bit (supported by some larger structures) in my #ActivityPub crate.

impl ApCollection {
    /// Creates an async stream over all items in this collection and its linked pages.
    pub fn stream_all<F, Fut>(self, fetcher: F) -> ApCollectionStream
    where
        F: Fn(&str) -> Fut + Send + Sync + 'static,
        Fut: Future<Output = Result<ApCollection, Box<dyn std::error::Error + Send + Sync>>>
            + Send
            + 'static,
    {
        ApCollectionStream::new(self, fetcher)
    }
}

I wanted to be able to (using async code) take an ActivityPub Collection and iterate over all of its members without worrying about pulling in each page. This implementation accomplishes that.

The full source is here.

# orderedItems[].object.content
For a long time, I was trying to make the entry of text in my #Enigmatick Compose component behave as rich text like I've seen in solutions like Notion or Confluence. But I don't think I'm a strong enough front-end coder to make that work; it was always buggy and problematic.

fn main() {
    println!("Here's my new approach: a live representation of the text displayed as readers will see it");
}

# orderedItems[].object.content
To save myself some hassle, I've taken to adding functions I want in a hurry to the server command itself.

/srv/enigmatick# ./enigmatick
A federated communication platform server

Usage: enigmatick <COMMAND>

Commands:
  init         Initialize folder structure for media files
  template     Generate .env.template file
  migrate      Run database migrations
  cache        Manage cached media files
  system-user  Create or ensure system user exists
  server       Start the web server and background tasks
  instances    Manage federated instances
  send         Send various activities
  muted-terms  Manage user muted terms
  help         Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version


/srv/enigmatick# ./enigmatick cache
Manage cached media files

Usage: enigmatick cache <COMMAND>

Commands:
  prune          Prune cached files older than specified duration (e.g., 30d, 2m, 1y)
  delete         Delete cached item by URL
  delete-server  Delete cached items from server/domain pattern
  help           Print this message or the help of the given subcommand(s)

Options:

  -h, --help  Print help

/srv/enigmatick# ./enigmatick send
Send various activities

Usage: enigmatick send <COMMAND>

Commands:
  actor-update  Send actor update to all known instances
  help          Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help

The commands here run with direct database access. But my plan is to extend a lot of this (and much more) as a network-based CLI tool (e.g., maybe as ek in the enigmatick_wasm crate).

#Enigmatick #RustLang #ActivityPub

# orderedItems[].object.content
I've built #Enigmatick to manage threading using conversation IDs like Mastodon uses. But that's not aligned with #ActivityPub - or at least it wasn't the least time I looked. ActivityPub uses inReplyTo to indicate direct parent relationships and doesn't have a concept for linking multiple objects under a single ID.

Other systems don't use the conversation convention and that's caused some discord in my timeline. So now I'm learning about closure tables and concepts I probably would already know if I had studied computer science in college. But it's never too late to learn!

# orderedItems[].object.content
I reworked my image processing on #Enigmatick recently but haven't had a chance to try it out. Here's a picture of my son checking in with the neighbor llamas a few years ago.

# orderedItems[].object.content
Migrated my followers from #Serendipitous to #Enigmatick. The process was pleasantly simple - even though I haven't implemented Move activities on the new system yet. I'll tackle that shortly as I see I have a backlog of those in my unprocessable queue.

It also seems like I need to expand my database connection pool; all of those Follow activities coming in at once was a little messy.

# orderedItems[].object.content
Using my amateur woodworking skills, I built myself a server rack using red oak and 72" metal rails last year. It lives in a dedicated corner of my basement where sound and heat are manageable challenges. It has since housed several enterprise switches (SONiC) and routers (VPP) I've been building around.

My venerable DS1513 NAS turned is now 12 years old, so I decided to leverage the rack to move to a Supermicro X11-based server running TrueNAS SCALE to replace it. 12 drives, 40 cores, 2 x 25Gb NICs, and 128GB of ECC RAM later, I may have over done it a little. But it's a very nice upgrade. I finished moving the NFS shares for this service over tonight and migrating to a new database run on the NAS server itself (which eliminates a long-standing pain point with iSCSI).

I'm considering just turning down my long-running Mastodon instance in favor of leaning in to #Enigmatick. As a result, I'm sending out lots of follow requests. Feel free to follow me back here, as this is what I'll be paying attention to moving forward.

# orderedItems[].object.content
#Enigmatick's timeline retrieval performance was getting a little slow, so I spent some time optimizing my PostgreSQL queries yesterday. I was able to take the EXPLAIN ANALYZE loop from 17 seconds (an exaggeration of the actual experience in production of a delay of about 2 seconds) to 53ms. It was mostly down to a JOIN that employs a JSONB_AGG to pull in Actor records associated with an Activity to avoid having to make additional calls. The way I was matching against the attributed_to JSONB column was apparently slowing things down.

I also have some #Wasm calls that proactively transform #E2EE asymmetrically encrypted messages to symmetrically encrypted vault items. Those run on each load of the timeline, but I was able to use spawn_local to move the processing to a background task for timeline views that don't display encrypted content.