Nextcloud Social is an ActivityPub app that connects your Nextcloud account to the Fediverse. Your instance acts as a lightweight federated social server: each user gets a Person actor, can write and edit posts, follow accounts on other servers, and like, boost and reply to what they receive.
It is a partial implementation of ActivityPub and of the Mastodon client API β enough for posting, following and reading timelines, but far from feature parity with Mastodon. See "Not implemented yet" below before deploying it as someone's only Fediverse client.
- π§ Timelines β Home, Local, Global (federated), Direct messages, Liked posts, Notifications, per-account and per-hashtag timelines.
- βοΈ Composer β write posts and replies, pick a visibility (public, unlisted, followers-only, direct), insert emoji, and attach images.
@mentionsand#hashtagstyped by hand are extracted from the text and turned into real recipients and tags. - βοΈ Edit posts β edit your own local posts inline; the change is saved and federated as an ActivityPub
Update(lib/Service/PostService.php,editPost()). - ποΈ Delete posts β delete your own posts.
β οΈ Content warnings β put a warning on a post in the composer and the body is folded away behind it until a reader asks to see it (it is not even in the page until then). Carried as the ActivityPub object'ssummaryand asspoiler_texton the client API, so warnings written elsewhere in the Fediverse are honoured here and vice versa.- π π π¬ Post actions β like/unlike, boost/unboost (
Announce) and reply. - π₯ Following β follow and unfollow local and remote accounts, and browse followers/following lists.
- π Pinned posts β pin up to five of your own posts to the top of your profile (
pin/unpinon the status-action endpoint,?pinned=trueon the account statuses route). Pins are published in the actor'sfeaturedcollection, so other Fediverse servers show them too; pinned posts of remote accounts are not fetched. - πΌοΈ Profiles β avatar, uploadable banner/header image, a profile description (
note) and up to four editable profile metadata fields (the name/value table under the bio), federated asPropertyValueattachments on the actor and shown for remote accounts too. - π Federation β signed HTTP delivery of
Create,Update,Delete,Like,Announce,Follow,AcceptandUndoactivities, an outbound request queue and a stream queue for resolving incoming objects, both drained by background jobs and byocc social:queue:process. - π Inbox forwarding β a reply to one of your posts that arrives from a stranger's instance is passed on to your followers, so everyone reading the thread sees the same one. Forwarded untouched and only when the reply carries its author's linked-data signature, so the servers receiving it verify the original author rather than trusting this one; private posts and their replies are never fanned out.
- π Live timelines with notify_push β when the notify_push app is installed, new timeline entries reach open web clients as push events and polling drops to a five-minute safety net; without it the client polls every 30 seconds.
- π Discovery & search β WebFinger lookups, remote actor resolution and caching, search over known accounts and hashtags, and full-text search of visible posts through Nextcloud's unified search (own posts, public content and messages addressed to you; a plain database substring match, no external search engine needed).
- π Trending hashtags β the tags used most on the instance, counted per window (1 h to 10 days) by the same cron job that maintains the hashtag index, listed in the app's sidebar and served as Mastodon
Tagentities at/api/v1/trends/tags. - π Bookmarks β bookmark any post from its menu and read them back under Bookmarks in the sidebar. Purely local, never federated.
- π Link previews β a post that links somewhere gets a preview card (OpenGraph, with the plain title/description as fallback) read by a background job, so nothing waits for a stranger's web server. The fetch is HTTP(S)-only on every hop, refuses local addresses, is size- and time-capped and obeys the instance access list. Cards are never federated β like Mastodon, every instance reads the page itself.
- π§Ή Retention β remote statuses older than
retention_days(default: disabled) that no local user interacted with are pruned together with their cached attachments, from cron orocc social:stream:prune; local content is never touched. Configurable in the Social section of the administration settings. - π§Ή Accounts are deleted together β removing a Nextcloud user takes their Fediverse account with it: the actor is tombstoned, what belongs to it is dropped, and a
Deleteis federated so the servers that cached it drop their copies too. Previously the Social account outlived the user, kept resolving over WebFinger and kept receiving deliveries. - π©Ί Federation health β the administration settings show what the outbound queue is doing: how many deliveries are waiting, how many keep failing, which instances they are stacked up against and how close each is to being given up on (a delivery is abandoned after 15 attempts, previously without a word to anyone).
occ social:queue:statusprints the same summary. - π‘οΈ Instance access list β an allow-list or deny-list of remote hosts, enforced on incoming activities and outgoing requests. Managed with
occ social:fediverse; see docs/OCC-Commands.md for the details and its limits. - π Mastodon-compatible API β the Mastodon client API's core surface plus OAuth 2 authorization: third-party clients can log in, read every timeline, post (with media and polls), follow/unfollow, favourite/boost/bookmark, search (
/api/v2/search), manage follow requests and report. No streaming endpoint or push subscriptions β clients poll. See docs/API.md for exactly which routes exist.
These are absent from the code today, not merely rough edges:
-
Blocking and muting are supported: block an account to sever the relationship in both directions and hide it everywhere (federated as a
Blockactivity unlessocc config:app:set social federate_blocks --value 0); mute an account to hide it from your timelines β and optionally notifications β without it ever knowing. Blocking and muting are done from an account's profile menu, and Settings β Blocked and muted accounts in the app's left sidebar lists both, with unblock/unmute inline. Exposed over the Mastodon API too (/api/v1/accounts/{id}/block|unblock|mute|unmute,/api/v1/blocks,/api/v1/mutes). -
Reporting is supported:
POST /api/v1/reportsfiles a report, incoming federatedFlagactivities are stored the same way, admins are notified and review reports (and manage the Fediverse access list) in the Social section of the administration settings. Reports are never forwarded to the reported account's instance. -
Locked accounts / approvable follow requests are supported:
PATCH /api/v1/accounts/update_credentialswithlockedtogglesmanuallyApprovesFollowers; incoming follows towards a locked account stay pending (with afollow_requestnotification) until the owner authorizes or rejects them via/api/v1/follow_requests(lib/Interfaces/Object/FollowInterface.php). -
Profile metadata fields are supported: up to four name/value pairs, edited on your own profile page or through
PATCH /api/v1/accounts/update_credentialswithfields_attributes, stored on the local actor row, federated asPropertyValueattachments and read back from remote actors (lib/Model/ActivityPub/Actor/Person.php). They carry no link verification βverified_atis alwaysnull. -
Polls are fully supported: write your own in the composer (up to four options, single or multiple choice, 30 minutes to a week), and view and vote on federated ones β votes federate to the poll's author as ActivityPub vote notes, incoming votes are counted and the new totals federate back as
Update{Question}(lib/Service/PollService.php). No lists. -
Media attachments cover images (JPEG, PNG, GIF, WebP β images get a resized preview and a blurhash), video (MP4, WebM, QuickTime) and audio (MP3, MP4/AAC, OGG/Opus, WAV, FLAC); see
filterMimeTypes()inlib/Service/CacheDocumentService.phpfor the exact list. Video and audio are stored as-is (no transcoding, no thumbnail β the player is the preview) and remote copies respect themax_sizeapp setting (default 10 MB). No document/file attachments. -
Custom emoji from other instances render β
Emojitags on remote statuses and actors survive the cache (via the stored wire source) and are served in theemojisfield of status and account entities; the web client shows them inline in content and display names. The instance has no custom emoji of its own:/api/v1/custom_emojisreturns an empty list (lib/Controller/ApiController.php,customEmojis()). -
No status translation. The
translateaction returns the post unchanged (lib/Service/ActionService.php). -
Media uploads go through
POST /api/v2/media(or v1) inlib/Controller/ApiController.php: jpeg/gif/png, with alt text viadescription, editable withPUT /api/v1/media/{id}and attachable to statuses viamedia_ids.
- Clone this repository into your Nextcloud
apps/directory. - Install the dependencies and build the frontend:
cd /var/www/nextcloud/apps/social
composer install # PHP dependencies
npm ci
npm run build # production bundle into js/- Enable the app in Nextcloud (
occ app:enable social) and check the install withocc social:check:install. - While working on the UI, use
npm run devfor a development build ornpm run watchto rebuild on change. TheMakefilewraps the same scripts (make build-js,make build-js-production,make watch-js,make lint). - To produce a release archive, run
./build-package.sh. It runscomposer install --no-dev,npm run build, copies the app without the dev files and writesbuild/artifacts/social.tar.gz.make appstorebuilds the same archive through the Makefile; despite thesign_dirname it only stages and tars, it does not sign anything.
That banner has two quite different causes, and the app now tells them apart.
The first is the one it names: the server does not answer /.well-known/webfinger.
Follow the documented redirects.
The second is that Social is set up for a different address than the server now
uses. Social reads overwrite.cli.url once, the first time the app is opened, and
builds every account id, post id and WebFinger answer from that stored copy
(social.cloud_url). Change the server's URL afterwards and the two drift apart in
silence: WebFinger answers for a host nobody asks about, and the app blames
.well-known when .well-known is fine.
Social reports the mismatch with both addresses but will not correct it, because the
stored address is baked into every id already written. Either point
overwrite.cli.url back at the address Social knows, or accept the rename and run
occ social:reset --uri=<new address>, which deletes everything Social holds.
To see the two values:
occ config:app:get social cloud_url
occ config:system:get overwrite.cli.urlBanner/header uploads work: the image is stored in the app's document cache, the
local actor's cached header is updated, and the change is federated as an actor
Update (lib/Controller/LocalController.php, uploadBanner()). A banner can also
be set from a URL. Only JPEG, GIF and PNG survive the mime filter.
If an uploaded banner does not appear immediately:
- Reload the profile page (clear the browser cache if necessary).
- Check the server log for
[LocalController] uploadBanner failedand forFailed to federate banner change, which is only a warning and does not undo the local upload. - Re-fetch the account data, or run
occ social:cache:refresh, to refresh the cached actor.
Backend and frontend unit tests run without a Nextcloud server or database.
composer install # PHPUnit + OCP interface stubs
composer run test:unit # PHP: vendor/bin/phpunit -c tests/phpunit.xml
npm ci
npm test # JS: vitest run (tests/js/** and src/**/*.test.js)
npm run test:coverage # with a coverage report in coverage/jsPHP tests live in tests/ mirroring lib/ (lib/Service/PostService.php β
tests/Service/PostServiceTest.php). Everything a class needs is mocked; code that
reaches the container statically finds a TestContainer behind \OC::$server
(see tests/Helper/TestContainer.php). Frontend tests use Vitest with
@vue/test-utils and jsdom; tests/js/setup.js provides the Nextcloud globals
(t, n, OC, OCA, localStorage, router webroots).
A second PHP suite in tests/Integration/ runs against a real Nextcloud and
database β migrations, query SQL and storage boundaries the mocks cannot reach.
CI runs it on SQLite, MySQL and PostgreSQL; locally, point it at an installed
server:
NEXTCLOUD_ROOT=/path/to/nextcloud composer run test:integrationSee tests/Integration/README.md for what it covers.
- Contributions welcome β open a pull request and run the build and tests locally
first (
npm run lint,npm test,composer run test:unit). - Reset local Social data for development with:
occ social:resetThis prompts twice and then empties every Social table. occ social:reset --uninstall additionally drops the tables, migrations, background jobs and app
config. See docs/OCC-Commands.md for all commands.
See the repository's license files in the LICENSES/ directory.
