Post, follow and reply across Mastodon, Pixelfed, PeerTube, GoToSocial and Akkoma β from the same place your files, calendar and chat already live. No new account. No new app. No algorithm. No advertising.
Nextcloud Social gives every user on your server a real Fediverse identity β
@you@your.cloud β that anyone on Mastodon can follow, mention and reply to. What you
write leaves your server signed, and lands in their timeline. What they write comes
back into yours. Nobody else holds it, nobody sells it, and nobody reorders it.
It is a partial implementation of ActivityPub and of the Mastodon client API: enough to post, follow, read, converse, moderate and run an instance β and honest about what it is not. Read Not implemented yet before you make it somebody's only Fediverse client.
One box, and everything a post can carry.
- Say who sees it. Public, unlisted, followers-only or direct, chosen per post from the default you set once. A reply inherits the audience of the post it answers, and starts addressed to everyone in the conversation rather than to one person.
- Pictures, video and audio. JPEG, PNG, GIF, WebP, AVIF and HEIC/HEIF straight off an iPhone (transcoded on the way in), MP4, WebM and QuickTime, MP3, AAC, Opus, WAV and FLAC. Video is never re-encoded and is streamed to storage a chunk at a time, so it gets a 2 GB ceiling rather than the 10 MB a picture is held to.
- And the files people actually have. PDF, text, Markdown, CSV, ZIP, EPUB, ODF
and the Office formats ride on a post too (
DOCUMENT_MIME_TYPESinlib/Service/CacheDocumentService.php), stored as they are and drawn as a card you press to download. On the wire they are an ActivityPubDocumentcarrying its mime type, which Mastodon shows as a link and another Nextcloud shows as a file. - Every picture is stripped of its metadata before it is stored or sent β Exif, XMP, IPTC, and the GPS coordinates a phone quietly attaches. Done on the container rather than by re-encoding, so nothing loses a generation of quality, and the colour profile is deliberately kept.
- Alt text that actually gets written. Every attachment has a description field, and a post carrying an undescribed picture says so before it goes. A nudge, never a refusal.
- A focal point. Click or drag a crosshair over a picture to say where the subject is, so a square crop never cuts somebody's head off. It federates, so other servers crop it correctly too.
- Seven adjustments, previewed live and baked into the copy that is posted.
- Polls. Up to four options, single or multiple choice. Votes federate both ways
and the new totals come back as an
Update. - Content warnings that fold the post away β and keep it out of the page entirely until a reader asks for it.
- Custom emoji. Type
:shortcode:and the post travels with a matchingEmojitag, so it renders on servers that have never heard of it. - A GIF picker backed by the instance's own library β no Giphy, no Tenor, nobody's tracker.
- Quote a post, with the original author's permission carried on the wire (FEP-044f).
- Say what language it is in, starting from your Nextcloud language.
- Send it later. Pick a time at least five minutes out and Post becomes Schedule.
- Mentions and hashtags typed by hand become real recipients and real tags.
My Feed, Local and Global are a switcher above the posts rather than three places to navigate to β one pill slides between them, and the arrow keys move through them. Photos and Videos carry the same switcher, so the pictures of the people you follow, of this instance and of the whole Fediverse are one click apart.
-
Photos and Videos are timelines of their own, drawn as grids and asked of the server rather than filtered out of a page you already have.

-
PeerTube, properly. A PeerTube video arrives with its title, its channel, its thumbnail and something to play. The video streams from the instance that holds it β a two-hour talk is not something to mirror onto somebody's Nextcloud β and is proxied, so nobody's IP address reaches a server they never chose to talk to. Posts that are one video go out as an ActivityPub
Video, the only shape PeerTube ingests. -
Blurhash placeholders, so a timeline never jumps as pictures load, and blurred previews for anything marked sensitive.
-
An ALT badge on every described picture, wherever it is drawn, that shows the description when pressed.
-
Lists, made and filled in Settings or from anybody's profile β and every Nextcloud group you are in is already a list, built and maintained by nobody.
-
Follow a hashtag and it reads exactly like following a person.
-
Filter out words you would rather not read β Settings β Filtered words. A filter is a handful of words, the timelines it applies in, and whether a matching post is folded away behind the filter's name, with a Show anyway, or taken out of the timeline altogether; it can be set to expire on its own. A folded post is not in the page at all until you ask for it, so nothing is read by accident. Nobody is told, nothing is deleted, and what is currently being taken away is said at the top of the page rather than left to be noticed as gaps in a conversation.
-
Bookmarks and favourites, each with a page of their own.
-
Announcements from your administrators at the top of the timeline, above the composer. An unread one interrupts; Got it marks it read for your account on every device, and it does not come back. What you have already read is kept out of the way behind one line, and an emoji reaction is the one thing you can say back.
-
Keyboard throughout:
jklfbrong, and?for the list. -
Live when notify_push is installed; polling every 30 seconds when it is not.
A post opened from a timeline gets a page of its own: the post, a reply box already pointed at it, the faces behind the boost and favourite counts, and the details a card leaves out β when exactly, to whom, in what language, whether it has been edited, and a link to the original for a post from another server.
Replies read as a conversation. Each reply is followed by the replies to it, oldest first, stepped in by how deep it sits with a line down its side. A reply whose parent this server does not hold keeps its place in time rather than disappearing. When the thread shown is shorter than the reply count, the page says so instead of presenting what it has as the whole conversation.
Four things you can do to a post: reply, boost, favourite, and react with an
emoji β which federates as EmojiReact and shows who reacted with what.
The hardest part of a new Fediverse account is the first ten follows. This answers it four ways:
- The people you already share a Nextcloud with. Every Nextcloud profile has a
fediversefield; this app fills in yours and reads everybody else's, so your colleagues are suggested before any algorithm has a thing to say. - Starter packs β a named handful of accounts with one button that follows all of them. Administrators curate their own.
- Trending hashtags, ranked over a window you choose from one hour to ten days. The busiest hour and the busiest ten days are genuinely different lists, not one list relabelled.
- Trending pictures and videos.
Search covers people, hashtags and the full text of every post you are allowed to see β your own, public content and anything addressed to you β through Nextcloud's own unified search. No external search engine to run.
- Posts, Photos and Videos as three tabs, each one a question asked of the server.
- A banner, a bio and four metadata fields, federated the way Mastodon does it and edited in Edit profile on your own profile.
- Verified links. A profile field naming a web page gets the tick when that page
links back with
rel="me"β from an<a>anywhere on it or a<link>in its head. The editor says which of your fields are verified and when each was last proved, and hands you the line to paste on the far end. The page is fetched in the background, once a day per account. - Featured hashtags. Up to ten tags pinned under the bio, saying what the account is about in its own words; a visitor clicks one and reads what was posted under it, with the count taken from the posts rather than stored. You set your own in Settings β Featured hashtags, which starts from the tags you already post with most instead of an empty box, and your profile links straight there.
- Highlights β a twelve-week posting chart and the tags somebody keeps coming back to.
- Pinned posts, up to five, published in the actor's
featuredcollection. Remote accounts' pins arrive too. - A grid or a timeline, whichever you last chose, cropped to each picture's focal point.
- A private note about somebody, for you alone. It never leaves this server and the person it is about is never told.
A page, not a list of everything. Filters across the top (All, Mentions, Favourites, Boosts, Follows, Polls, Edits), a New line where you left off, and a run of likes of the same post drawn as one card with the faces stacked rather than the same post quoted twelve times.
It marks itself read after it has been in front of you for two seconds, not the instant it renders β so it stops clearing the badge on your phone for things nobody saw.
Nextcloud's own bell rings too. Mentions, favourites, boosts, new followers, follow requests, edits of posts you boosted and polls you voted in all reach Nextcloud notifications and its mail digest, each linking into this app rather than out to a remote server. A follow request carries Accept and Decline on the bell entry itself.
Everything about your account in one page: the name you publish under, whether people must ask before they follow you, whether other servers may suggest you and index your public posts, whether this is an automated account, and the audience every new post starts with. Only what you changed is sent, so a display name your Nextcloud gets from elsewhere is never written back.
Below it: your lists, your scheduled posts with a way to cancel one, your blocked and muted accounts with unblock and unmute inline, and migration.
Migration takes your account with you. Export writes your profile, follows,
followers, blocks, mutes, bookmarks, likes and every post you have written to a zip β
and the pictures and videos come with it, copied into the archive in the layout
Mastodon's own export uses, with each attachment pointing at the copy rather than at
the server you are leaving. Import reads one back, including an archive from
occ user:export. A third section brings your follows over from Mastodon, Pixelfed,
GoToSocial or Akkoma via their following_accounts.csv. Your private key is
deliberately not in the archive.
Under 600px the avatar moves inside the card, the card takes the width of the screen, the composer's toolbar wraps instead of pushing Post off the edge, and a post's page gives up the column it kept for an avatar that is no longer beside it. Dark mode is the same app, not a second design.
- Share to Social, from Files. Select a picture or a video β up to ten β pick Share to Social from the menu, and the composer opens with them already attached. Nothing is uploaded a second time.
- Links unfurl. Paste a link to a post or a profile into a Talk message, a Text document or a Deck card and it becomes a card with the author, the text and the first picture. Only what anybody could read is rendered, because the card is cached once for everyone who sees the link.
- Nine Dashboard widgets and an entry in the contacts menu.
- The Activity app lists your follows, mentions, boosts and favourites, and puts them in the Activity digest mail. Activity's own notifications stay off: the bell is the bell.
- Unified search, so Social posts turn up where every other search result does.
- Deleting a Nextcloud user takes their Fediverse account with it β tombstoned,
dropped, and a
Deletefederated so other servers drop their copies too.
- Block to sever a relationship in both directions and hide somebody everywhere.
- Mute to hide them from your timelines and optionally your notifications without them ever knowing β for an hour, a day, seven days, thirty, or until you lift it. A profile says when a timed mute runs out.
- Both are offered on the profile and in the menu of any post they wrote, which is usually where you decided.
- Report an account or a post. With forwarding on, a report about a remote account also reaches the instance that hosts it β anonymised, and signed as this server rather than as the person who filed it, because they would otherwise be handing their handle to the very instance they are complaining about.
- Locked accounts, so follows must be approved, with a Follow requests page.
- Keyword filters, written and read in Settings β Filtered words: they are yours alone, they apply to every timeline this app draws, and a filter set months ago from a phone is finally visible from here.
- Per-user domain blocks and conversation mute, through the API.
- Nothing is sent to a third party. No geocoder β a place on a post is one this instance has seen or one you name yourself, because sending somebody's location to a stranger at the moment they are deciding whether to publish it is exactly the failure the Exif stripping exists to prevent. Link previews are read by this server, never federated, and never fetched from a local address.
Everything in Administration β Social, built out of the same components as the rest of the administration settings:
- Reports with Silence, Suspend, Lift and take-a-post-down, each recorded with the moderator who did it and written to Nextcloud's audit log. Suspending deletes what the account posted here, and the confirmation says so.
- An account browser over every account this instance knows, with the standing decision and the strike history against each one.
- Federation health β how many deliveries are waiting, how many keep failing, which
instances they are stacked up against, how close each is to being abandoned (16
attempts), and which instances have been given up on in the last seven days.
occ social:queue:statusprints the same summary andocc social:queue:retry --instance HOSTputs one host's deliveries back. - Server settings with an interface β contact address, instance description, upload
ceilings, inbox rate limit, secure mode, whether the block list is published, whether
self-signed certificates are accepted. Every one of these used to be an
occ config:app:setkey that almost nobody set. - Announcements β a notice to the whole instance, optionally between a start and an end, that everybody reads at the top of their timeline and dismisses once. Retention and the instance access list (an allow-list or a deny-list of remote hosts, enforced both ways) are here too.
- Domain blocks that clean up. Adding a host to the deny list also removes what it already sent: its accounts, their posts, the follows in both directions and the deliveries still queued towards it.
- Setup checks in Administration β Overview β whether
.well-known/webfingeranswers, whether the address Social builds its ids from is still the server's, whether the delivery job has run lately, and whether anything is stuck.occ social:check:installruns the same four and exits non-zero, so a deployment script can ask.
A statistics page for the instance, and Mastodon's admin API β accounts, reports, domain blocks, IP and email-domain blocks, trends, measures and retention β for anything you would rather automate.
Retention keeps the database honest: remote statuses older than retention_days that
nobody here interacted with are pruned with their attachments, and cached accounts
nobody follows are evicted after cache_actor_days. Local content is never touched.
occ social:media:usage says what the media is costing and how much of it is somebody
else's.
- The Mastodon client API, core surface plus OAuth 2: every timeline, posting with media and polls, follows, favourites, boosts, bookmarks, search, follow requests, reports, filters, conversations, markers, announcements, edit history and the admin API. See docs/API.md for exactly which routes exist.
- Pixelfed's own routes β the
/api/v2/configbootstrap its app reads on launch and thev1.1discover namespace. Every limit in the config is derived from the one the server actually enforces. - Full ActivityPub delivery: signed HTTP for
Create,Update,Delete,Like,Announce,Follow,Accept,Undo,Block,FlagandEmojiReact, an outbound queue and a stream queue, both drained by background jobs and byocc social:queue:process. - Inbox forwarding, so a reply from a stranger's instance reaches your followers β forwarded untouched and only when it carries its author's linked-data signature.
- 26
occcommands, documented in docs/OCC-Commands.md.
Important
Third-party Mastodon clients cannot reach the API yet. Every route is served
under /apps/social/, and the Mastodon client protocol has no way to be told about a
non-root API base β so a client given your domain looks for /api/v1/... and finds
nothing. Serving those paths at the domain root is the one thing standing between
this and stock clients. See
docs/Mastodon-Compatibility.md.
These are absent from the code today, not merely rough edges:
- No status translation. The
translateaction returns the post unchanged (lib/Service/ActionService.php). - No streaming API and no push subscriptions. Third-party clients poll. (The web client does get live timelines when notify_push is installed β that is a Nextcloud channel, not a Mastodon one.)
- 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
make appstore(or./build-package.sh, which is now a three-line wrapper around it β two copies of the exclusion list had already drifted apart, so there is one). It installs from the lock files, builds the frontend, stages the app without its development files and writesbuild/artifacts/social.tar.gz. It refuses to package whenjs/.htaccessis missing: that is a committed file rather than webpack output, and the target used to deletejs/wholesale before building. 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. It
asks twice; add --force to run it from a script (without it, --no-interaction
refuses rather than quietly doing nothing).
To see the two values:
occ config:app:get social cloud_url
occ config:system:get overwrite.cli.urlAdministration β Overview reports both of these β and two more things that break federation quietly β without anybody having to open Social. See docs/Admin.md for the whole setup, every configuration key and what to watch.
Banner/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. The upload goes through the same filterMimeTypes() list as an
attachment (lib/Service/CacheDocumentService.php), so every image type an
attachment may have is accepted β JPEG, PNG, GIF, WebP, AVIF and HEIC/HEIF (the
last converted on the way in); nothing narrows that list to pictures for a banner.
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/js
npm run typecheck # tsc over the plain-JS half of src/, against src/types/PHP tests live in tests/ mirroring lib/ (lib/Service/PostService.php β
tests/Service/PostServiceTest.php). Everything a class needs is mocked; the
server code that resolves a service statically β OCP\Server::get(), which
Response itself calls on every render β finds a TestContainer instead, which
knows a silent logger and a session with nobody in it and raises a named error
for anything else (see tests/Helper/TestContainer.php). Migration steps are
given a FakeTable that records the schema they ask for rather than building it
(tests/Migration/). 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.
tests/e2e/ drives a real Nextcloud with the app installed, in Chromium,
through Playwright: sign in, open the app, write a
post and see it in the feed, walk the Discover page, switch scopes, open a
group list. Nothing is mocked, and the bundle under test is the committed one
in js/. .github/workflows/e2e.yml sets up a throwaway server for every pull
request; against an instance of your own:
npx playwright install chromium
E2E_BASE_URL=https://cloud.example E2E_USER=alice E2E_PASSWORD=β¦ E2E_GROUP="Design" npm run test:e2e
E2E_GROUP is optional β the display name of a Nextcloud group the account is
in, for the list test; without it that test is skipped.
- 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.
- docs/Admin.md is the administrator's guide: what federation needs before it works, every app configuration key with its meaning and default, the sections of the administration page, how moderation is recorded, and the occ commands by task.
- docs/Mastodon-Compatibility.md answers how close this is to Mastodon in the three senses that can mean β whether its clients work, whether peers can tell the difference, and whether an instance could move onto it. Its last section is the backlog that follows: everything still between this app and a full replacement, in tiers, with what each item actually fixes and whether it is done.
- docs/User-Guide.md is the guide for the people using the app: getting an account, following, posting, reading, managing the account, keyboard shortcuts.
- Before picking up refactoring work, read docs/Technical-Debt.md β what in the app is old, borrowed or load-bearing, and what changing it would cost β and docs/Performance.md, which lists the query and scalability problems that are still open and the ones that have been fixed. Neither is checked by a test, so re-verify a claim before acting on it and update the file in the same change as the code.
See the repository's license files in the LICENSES/ directory.
Admin guide Β· User guide Β· API Β· occ commands Β· Architecture Β· Mastodon compatibility
Screenshots are of a development instance with seeded demo accounts.














