# linky: an open-source builder for a personal homepage you host yourself

> Linky is a Next.js application for building a dynamic personal page, with authentication, an API on Hono and Cloudflare Workers, analytics and email, and a self-hosting path that includes a Postgres container. The interesting part for an adopter is that the configuration is almost entirely environment variables, and one of them switches on a paywall.

**trylinky/linky** — The dynamic personal homepage builder

- Repository: https://github.com/trylinky/linky
- Website: https://lin.ky
- Stars: 800 · Forks: 79
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/trylinky-linky

## A full application, not a static site generator

Linky is described as an open-source dynamic personal page builder, and the word dynamic is doing the work. The tech stack listed in the README is a complete application stack rather than a build-time site generator. Next.js is the framework, TypeScript the language, Tailwind the CSS, better-auth the authentication, Hono on Cloudflare Workers the API, Resend the email, and Vercel the frontend deployments. That combination describes an application with users, sessions and a database, not a folder of Markdown files and a build script. The distinction matters for what you are adopting. A static generator gives you a site; this gives you the machinery of a hosted service, which is why the self-hosting section talks about Postgres, S3 asset uploads and OAuth credentials. The live instance is at lin.ky, and the README links a video of it.

## The configuration is a wall of environment variables

The .env.example at the repository root is the most informative file for anyone evaluating this, because it lists every third-party service the application expects to talk to. There is a core block with NEXT_PUBLIC_APP_ENV, NEXT_PUBLIC_BASE_URL and NEXT_PUBLIC_ROOT_DOMAIN. Then there is a set of service blocks, each commented with what it is for: Tinybird for analytics with a tracker token, Resend for emails with an API key, Sentry with a DSN, AWS with credentials and a region for uploading assets to S3, Google with client id and secret for OAuth, Twitter with client id, secret, a challenge and a callback URL, Stripe with a secret key and a webhook secret, and a Google Search Console HTML-tag verification token marked optional. The database is a Postgres URL, given as DATABASE_URL and DIRECT_URL. The scale of that list is the real cost of the project, because each entry is a service you either operate or pay for.

## An encryption key and a paywall switch are the two that matter

Two entries in that file deserve more attention than the rest. The first is ENCRYPTION_KEY, described as a 32 byte key used to encrypt and decrypt data. There is no default and no example value, so it must be generated per deployment, and getting it wrong has consequences that are not recoverable after the fact if it is used to encrypt user data. The second is PAYWALL_ENFORCED, with a comment that is unusually candid about the split between the two modes. Setting it to true enforces Free plan limits covering pages, blocks, analytics, private pages, verification and custom domains, and anything else reports paid limits for everyone. The comment adds that self-hosters usually leave this unset. That single flag is the difference between running an unrestricted instance and running one that reports a paid-tier limit set to nobody in particular, and it tells you the project was built around a hosted product with a freemium model.

## Origin handling is the security-sensitive part of the setup

The environment file explains credentialed cross-origin requests, which is where the care is. APP_FRONTEND_URL is the frontend origin this API serves, and the comment says it is always trusted for credentialed, cookie-bearing, cross-origin requests. TRUSTED_ORIGINS is optional and is a comma-separated list of extra origins allowed to make credentialed requests to the API, with the instruction to set it when self-hosting on your own domains, and the note that when set it replaces the built-in hosted defaults. There is a second clarification that matters in practice: published pages on user custom domains do not need to be listed, because they only call public, session-free endpoints. That single design decision is what makes custom domains work without asking every self-hoster to enumerate them, and it is the kind of detail that shows the difference between a project that was self-hosted by its author and one that was not.

## Local development expects a Postgres container on port 5432

The repository ships a docker-compose.yml for local development, and it is a single service. The image is postgres:15.1 with restart always, and the environment sets POSTGRES_USER glow_user, POSTGRES_PASSWORD and POSTGRES_DB glow_development, with a named volume for the data directory and port 5432 published to the host. The matching connection strings appear in the environment file:

```bash
DATABASE_URL=postgresql://glow_user:KGfUZosCOm@localhost:5432/glow_development
DIRECT_URL=postgresql://glow_user:KGfUZosCOm@localhost:5432/glow_development
```

The naming is worth noting rather than glossing over. The package is @trylinky/linky and the application is Linky, but the database user, database and volume are all called glow, and the LICENSE link in the README points at a repository under the tryglow organisation. This is a codebase with a previous name, and the database identifiers in the compose file have not been renamed along with it. It is not a functional problem, since the names are internal, but it will confuse anyone who greps for glow expecting to find something else. The root package.json also carries a dev:reset script that drops and recreates the public schema and then re-pushes and re-seeds, which is the fastest way to recover from a bad migration.

## A pnpm and Turborepo monorepo with a workspace per app

The repository layout is a workspace with apps/ and packages/ directories and a pnpm-workspace.yaml, driven by Turborepo. The root package.json is private, named @trylinky/linky at version 1.0.0, with pnpm 9.15.0 pinned as the package manager and Turbo 2.3.3 as its only runtime dependency. Every task is a turbo passthrough: turbo dev, turbo test, turbo lint and turbo typecheck, so each workspace owns its own scripts and the root just orchestrates them. There are also database-specific filters, dev:push runs pnpm --filter @trylinky/db push and dev:seed runs the seed script for the same package, which tells you the database package is a workspace member with its own tooling rather than a script living in the root. The repository also has an eslint.config.mjs, a prettier.config.js with the sort-imports plugin from trivago, a tsconfig.json and an .nvmrc.

## Self-hosting is documented, and there is a real caveat

The README states that you can self-host Linky if you would like to run it yourself, and points at two documents in the docs/ directory, local-development.md and self-hosting.md. That is the entire installation guidance in the README, which is reasonable for a project of this shape but means the actual setup steps live in files this summary does not reproduce. The structure of those two documents is the useful part. One covers getting it running on a developer machine, which is where the compose file and the .env.example belong. The other covers running it as a service, which is where the Worker deployment, the S3 bucket, the OAuth applications and the Stripe webhook come in. Two other files signal how the project handles its own governance. SECURITY.md is the route for reporting a security issue privately, and CONTRIBUTING.md covers getting set up and what CI will check.

## Conclusion

Adopt linky if you want a hosted-style personal page with accounts, custom domains and analytics but would rather run the instance yourself than use a hosted product. Do not adopt it if you want a static single page, because this is a database-backed application with auth, payments and email integrations. Before you commit, read docs/local-development.md and docs/self-hosting.md, set a real ENCRYPTION_KEY rather than leaving it empty, and check what PAYWALL_ENFORCED does in your deployment, because it changes the limits every user sees.

## FAQ

### What is linky?

It is an MIT licensed open-source dynamic personal page builder, deployed at lin.ky, built with Next.js, TypeScript, Tailwind, better-auth for authentication, Hono on Cloudflare Workers for the API, Resend for email and Vercel for frontend deployments.

### How do I self-host linky?

The README points at docs/local-development.md for running it locally and docs/self-hosting.md for running it as a service. The repository ships a docker-compose.yml with a postgres:15.1 service published on port 5432, and a .env.example listing every service the application expects, from the database URL to OAuth, S3, Stripe and analytics credentials.

### What does PAYWALL_ENFORCED do in linky?

Setting it to true enforces Free plan limits covering pages, blocks, analytics, private pages, verification and custom domains, while any other value reports paid limits for everyone. The environment file notes that self-hosters usually leave it unset.

### Does linky need a license file or a release?

The README links its LICENSE file to a repository under the tryglow organisation, and the project has no GitHub releases. The last push was on 2026-09-28 and the root package version reads 1.0.0, with pnpm 9.15.0 pinned and Turborepo running the dev, test, lint and typecheck tasks.

## Sources

- [Issues](https://github.com/trylinky/linky/issues)
- [License: MIT](https://github.com/trylinky/linky/blob/main/LICENSE)
- [Project website](https://lin.ky)
- [README](https://github.com/trylinky/linky/blob/main/README.md)
- [trylinky/linky on GitHub](https://github.com/trylinky/linky)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/trylinky-linky
