vutuv is a free, fast and open source social network service to host and share information about humans and organizations. It's hosted at https://vutuv.de.
We use MIT License.
vutuv is a Phoenix Framework 1.8 application. Install the following prerequisites using mise (see .tool-versions):
- Erlang 27.3.4.8
- Elixir 1.20.0-rc.6-otp-27
- PostgreSQL 17
Create config/dev.secret.exs:
import Config
config :vutuv, VutuvWeb.Endpoint,
secret_key_base: "generate-with-mix-phx-gen-secret"mix deps.get
mix assets.setup # install esbuild + tailwind
mix ecto.create
mix ecto.migrate
mix phx.serverVisit http://localhost:4000.
Emails are displayed in the browser via Swoosh's mailbox preview at http://localhost:4000/sent_emails.
Every vutuv email is machine-generated, so all of it carries the Auto-Submitted: auto-generated (RFC 3834) and X-Auto-Response-Suppress: All headers to keep out-of-office and other auto-responders silent. Mail is built from Vutuv.Notifications.Emailer.base_email/0 and sent through the single Emailer.deliver/1 chokepoint, the only place allowed to call Vutuv.Mailer.deliver/1.
Flag your account as admin:
UPDATE users SET administrator = true WHERE id = <user_id>;Admin panel: http://localhost:4000/admin
- Views: Phoenix 1.8 HTML modules with
embed_templates(nophoenix_viewdependency) - Routes: Verified routes (
~p"..."sigils) - Forms:
<.form>component with<.inputs_for>for nested forms - Assets: esbuild + Tailwind CSS v4
- HTTP server: Bandit
- Email: Swoosh with compile-time EEx text templates; all mail built from
Emailer.base_email/0and sent through oneEmailer.deliver/1chokepoint that stamps the auto-generated robot headers - Images: avatars and URL screenshots are stored on local disk and resized with
image(libvips); seeVutuv.Avatar/Vutuv.Screenshot - URL screenshots: rendered by local headless Chromium, wrapped in a browser window frame (
Vutuv.BrowserFrame) and stored as WebP; seeVutuv.PageScreenshot. Needs achromium/chromebinary on the host (setCHROMIUM_PATHif it is not on$PATH)
Business logic is organized into Phoenix context modules under lib/vutuv/:
| Context | Schemas | Purpose |
|---|---|---|
Vutuv.Accounts |
User, Email, Slug, SearchTerm, OAuthProvider, LoginPin, Locale, Exonym | Registration, PIN-based authentication, user management |
Vutuv.Profiles |
Address, PhoneNumber, SocialMediaAccount, Url, WorkExperience, UserSkill, Skill, Endorsement | User profile data |
Vutuv.Social |
Connection, Group, Membership | Following, groups |
Vutuv.Tags |
Tag, UserTag, UserTagEndorsement | Tagging and endorsements |
Vutuv.Recruiting |
RecruiterPackage, RecruiterSubscription, Coupon | Recruiter subscriptions |
Vutuv.JobPostings |
JobPosting, JobPostingTag | Job listings |
Vutuv.Search |
SearchQuery, SearchQueryRequester, SearchQueryResult | Search functionality |
Vutuv.Notifications |
Emailer, Cronjob | Email notifications |
mix testDeployment is automatic. Two GitHub Actions workflows drive it:
- CI (
.github/workflows/ci.yml) runsmix precommit(compile with--warnings-as-errors, unused-deps, format,credo --strict, tests) on every pull request and on pushes tomain. - Deploy (
.github/workflows/deploy.yml) runs on every push tomain. So merging or pushing anything tomainships it to production; there is no separate deploy command.
The Deploy job runs on the self-hosted vutuv3 runner (on bremen2) and executes scripts/deploy.sh, which builds a prod release, runs migrations against vutuv3_prod, atomically flips the current symlink, and restarts the vutuv3 systemd service. A deploy-production concurrency group ensures two production deploys never overlap. nginx is not touched by the script.
These tasks operate on the on-disk uploads under <UPLOADS_DIR_PREFIX>/... (see config/runtime.exs). They are meant to be run manually on the server.
mix avatar.optimizere-compresses the large JPEG avatar variants in<UPLOADS_DIR_PREFIX>/avatars/. Requires the ImageMagickconvertandguetzlibinaries on the host's$PATH.mix urls.create_screenshots(re)renders URL screenshots. Needs the headless Chromium binary already described above (setCHROMIUM_PATHif it is not on$PATH).