Skip to content

Latest commit

 

History

125 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

linear.gratis

Release License: MIT Next.js Supabase Cloudflare Workers Linear API

Free, open-source Linear feedback infrastructure: customer request forms, public issue views, public roadmaps, custom domains and branded portals backed by your own Linear workspace.

linear.gratis is built for teams that already run product work in Linear but do not want to pay for a separate feedback portal just to collect requests, share progress, or publish a roadmap.

Contents

Features

  • Customer request forms that create Linear customers and issues.
  • Public Linear views for sharing filtered issues with customers or stakeholders.
  • Public roadmaps with voting, comments, password protection and expiry support.
  • Custom domains for forms, views and roadmaps.
  • Branded public pages with logos, colours, favicons and footer settings.
  • Organisation-scoped access control backed by Supabase RLS.
  • Organisation-scoped Linear connections instead of user-scoped token reads.
  • Encrypted Linear API token storage with lazy legacy-token rotation.
  • Schema-level publishable-resource model with cross-type slug uniqueness.
  • SQL migration, rollback and invariant-test coverage for the core tenancy model.

Architecture

Layer Technology
App Next.js 15 App Router, React 19, TypeScript
UI Tailwind CSS, Radix UI primitives, Lucide icons, Framer Motion
Auth and database Supabase Auth, Postgres and RLS
Linear integration Linear GraphQL API and @linear/sdk
Runtime OpenNext for Cloudflare Workers
Deployment Wrangler and Cloudflare Workers static assets

The app is intentionally server-heavy around Linear access. Linear tokens stay on the server, are encrypted at rest, and are resolved through organisation-owned connection rows before API calls are made.

Getting Started

Prerequisites

  • Node.js 20 or newer
  • npm
  • Docker, if you want to run Supabase locally
  • Supabase CLI, for local database resets and migration work
  • Wrangler, installed through project dependencies
  • A Linear workspace and Linear API token
  • A Cloudflare account, if deploying the Worker or using custom-domain checks

Install

git clone https://github.com/curiousgeorgios/linear-gratis.git
cd linear-gratis
npm install
cp .env.example .env.local

Generate an encryption key for stored Linear tokens:

openssl rand -base64 32

Fill out .env.local, then start the app:

npm run dev

Open http://localhost:3000.

Environment Variables

The local template lives in .env.example.

Variable Required Purpose
NEXT_PUBLIC_SUPABASE_URL Yes Supabase project URL used by browser and server clients.
NEXT_PUBLIC_SUPABASE_ANON_KEY Yes Supabase anon key for client-side authenticated access.
SUPABASE_SERVICE_ROLE_KEY Yes Server-only key for API routes and trusted migration-style operations.
ENCRYPTION_KEY Yes Base64 32-byte key used to encrypt Linear API tokens.
IP_HASH_SALT Yes in production Stable random salt for pseudonymous public-roadmap IP metadata. Voting and comments remain available if it is absent.
CLOUDFLARE_API_TOKEN For custom domains Cloudflare API token for DNS and hostname verification checks.
CLOUDFLARE_ACCOUNT_ID For custom domains Cloudflare account id used with the API token.
CLOUDFLARE_ZONE_ID For custom domains Cloudflare zone ID used to manage custom hostnames.
FEEDBACK_WEBHOOK_SECRET For signed feedback Shared HMAC secret for inbound public-view feedback webhooks.
NEXT_PUBLIC_APP_DOMAIN Recommended Canonical app domain used for custom-domain CNAME instructions.
SUPABASE_DB_HOST Deployments Production database host used only by the migration gate.
SUPABASE_DB_PORT Deployments Production database port, normally 5432.
SUPABASE_DB_NAME Deployments Production database name, normally postgres.
SUPABASE_DB_USER Deployments Project-scoped production database user.
SUPABASE_DB_PASSWORD Deployments Production database password. Never expose this to previews or the Worker runtime.

Never expose SUPABASE_SERVICE_ROLE_KEY, ENCRYPTION_KEY, or Cloudflare tokens to the browser. Infisical prod is the source of truth for production secrets; the corresponding Worker secrets are deployment copies. Cloudflare cannot reveal an existing secret value, so migrate a Worker-only secret by recovering it from its original secure source or by coordinating a rotation with its consumers.

Database Migrations

Forward migrations live in supabase/migrations. Manual rollbacks live in supabase/rollbacks.

Production releases use Supabase's standard supabase_migrations.schema_migrations ledger. The release runner takes a PostgreSQL advisory lock, performs a dry-run preflight, applies pending migrations, and verifies the ledger again before Wrangler can deploy the Worker. It intentionally does not use --include-all: missing or out-of-order history stops a release and requires an explicit audited repair.

Important: do not move rollback SQL into supabase/migrations. Supabase applies every SQL file in that directory as a forward migration.

For a fresh local database:

supabase start
supabase db reset

For a linked Supabase project:

supabase db push --linked

Recent schema work is documented in:

SQL Invariant Tests

The SQL tests exercise the core migration invariants introduced by migrations 019-023. The runner refuses non-local database hosts and every SQL file ends in ROLLBACK:

TEST_DATABASE_URL="postgresql://postgres:[email protected]:54322/postgres" \
  bun run test:db

Testing

The default suite is deterministic and offline. It covers business rules, cryptography, redaction, Linear pagination with mocked network responses, data and schema invariants, UI render contracts, and security controls across every API route. Fuzz and metamorphic cases use fixed seeds so failures reproduce.

bun test
bun run test:coverage
bun run test:stress
bun run test:smoke
bun run test:all

test:coverage enforces an explicit manifest of production modules at 80% minimum per-file line/function coverage and 95%/90% overall loaded-source line/function coverage. test:all adds type checking, lint, a production build, an OpenNext Cloudflare Worker bundle, and headless smoke checks of the generated marketing and authentication routes. Database tests remain separate because they require a disposable local Supabase instance. The GitHub Actions test workflow runs both layers for every pull request and every push to main.

Scripts

Command Description
bun test Run deterministic unit, property, contract, and render tests.
bun run test:coverage Run tests and enforce production-module coverage thresholds.
bun run test:stress Repeat every test file 20 times to expose order or lifecycle flakiness.
bun run test:smoke Build and visit representative production pages in headless Chromium.
bun run test:db Run rollback-only SQL invariants against TEST_DATABASE_URL on localhost.
bun run test:all Run coverage, types, lint, build, and browser smoke gates.
bun run db:migrate:production Lock, preflight, apply, and verify production migrations using Infisical-injected database settings.
bun run release:build Run the complete production release gate.
bun run release:deploy Apply migrations and deploy the already-built Worker, in that order.
npm run dev Start the Next.js development server.
npm run build Build the Next.js app.
npm run build:worker Build the Cloudflare Worker bundle through OpenNext.
npm run preview Build the Worker and run it locally with Wrangler.
npm run deploy Build and deploy the Worker with Wrangler.
npm run cf-typegen Generate Cloudflare environment types.
npm run generate-og Generate Open Graph assets.

Deployment

This project deploys as a Cloudflare Worker via OpenNext. The Worker config is in wrangler.jsonc.

bun run deploy

Manage or rotate the value in Infisical first, then mirror it to the Worker without committing it. The deploy script builds, tests, migrates, and deploys with Infisical prod injected. If migration preflight or verification fails, Wrangler is never invoked.

Cloudflare Workers Builds is connected to the GitHub main branch for automatic production releases. Its production build command is bun run release:cloudflare:build and its deploy command is bun run release:cloudflare:deploy; both fetch their environment from Infisical before invoking the underlying release stages. Cloudflare's build image runs coverage, types, lint, and the Worker build; its deploy stage then waits for the same commit's required GitHub application/browser and database checks. A failed, cancelled, missing, or timed-out GitHub check prevents migrations and deployment. The only Cloudflare build secret is a read-only, production-scoped INFISICAL_TOKEN; application and database secrets remain in Infisical. Preview builds are disabled so pull requests and forks cannot receive production credentials or run production migrations. The production trigger pins BUN_VERSION=1.2.21 to match the repository and GitHub Actions coverage runtime. The self-hosted Infisical API domain is public configuration and is pinned in .infisical.json; tokens and secret values are never stored there.

infisical secrets set --env=prod SECRET_NAME=@/path/to/secure-value
npx wrangler secret put SECRET_NAME

NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY are public configuration values and may live in Wrangler vars; they are also available in the Infisical project so release builds have one consistent configuration source.

Project Structure

src/
  app/                 Next.js routes and API handlers
  components/          Shared UI and product components
  contexts/            Client-side auth state
  data/                Static marketing and SEO content
  lib/                 Supabase, Linear, auth, crypto and domain helpers
supabase/
  migrations/          Forward-only database migrations
  rollbacks/           Manual rollback scripts, ignored by Supabase migration flow
  tests/               SQL invariant tests
  contracts/           Future contract-phase migration drafts
docs/
  adr/                 Architecture decision records and runbooks
tests/                 Unit, property, contract and render suites

Security

  • Linear API tokens are encrypted at rest and decrypted only in server-side code.
  • Public password access cookies are signed, scoped and tied to the stored hash.
  • Public routes enforce active, expiry and password gates server-side.
  • Supabase RLS scopes authenticated resources through organisation membership.
  • Service-role code paths re-check organisation membership before mutating data.

Please do not open public issues for suspected vulnerabilities. Email [email protected] with enough detail to reproduce the problem.

Contributing

Contributions are welcome. For non-trivial changes, open an issue or discussion first so the schema, security and deployment implications can be worked through.

  1. Fork the repository.
  2. Create a branch from main.
  3. Make the smallest coherent change.
  4. Run the relevant verification commands.
  5. Open a pull request with the behavior change, migration impact and test coverage called out clearly.

Before submitting database changes, check:

  • Forward migrations are in supabase/migrations.
  • Manual rollbacks are in supabase/rollbacks.
  • New invariants have SQL coverage in supabase/tests.
  • Public-surface changes do not expose secrets, hashes, tokens or private Linear metadata.

Self-Hosting Notes

By default, every user belongs to a personal organisation and resources are scoped to organisation members. That is the correct default for a public SaaS deployment.

For a private agency or internal fork where every account on the instance is a trusted colleague, you may want one shared organisation. Run the recipe below only after migration 015_organisations.sql has applied. This is fork-specific configuration, not part of the standard migration suite.

BEGIN;

-- 1. Create one shared organisation owned by the oldest user on the instance.
INSERT INTO organisations (id, name, slug, created_by)
VALUES (
  gen_random_uuid(),
  'Team',
  'team',
  (SELECT id FROM profiles ORDER BY created_at LIMIT 1)
);

-- 2. Add every user as a member.
INSERT INTO organisation_members (organisation_id, user_id, role)
SELECT
  (SELECT id FROM organisations WHERE slug = 'team'),
  id,
  CASE
    WHEN id = (SELECT id FROM profiles ORDER BY created_at LIMIT 1)
      THEN 'owner'::org_role
    ELSE 'member'::org_role
  END
FROM profiles;

-- 3. Point every existing resource at the shared org.
UPDATE public_views           SET organisation_id = (SELECT id FROM organisations WHERE slug = 'team');
UPDATE customer_request_forms SET organisation_id = (SELECT id FROM organisations WHERE slug = 'team');
UPDATE branding_settings      SET organisation_id = (SELECT id FROM organisations WHERE slug = 'team');
UPDATE custom_domains         SET organisation_id = (SELECT id FROM organisations WHERE slug = 'team');
UPDATE roadmaps               SET organisation_id = (SELECT id FROM organisations WHERE slug = 'team');

-- 4. Drop now-empty personal organisations.
DELETE FROM organisations o
WHERE NOT EXISTS (SELECT 1 FROM public_views           WHERE organisation_id = o.id)
  AND NOT EXISTS (SELECT 1 FROM customer_request_forms WHERE organisation_id = o.id)
  AND NOT EXISTS (SELECT 1 FROM branding_settings      WHERE organisation_id = o.id)
  AND NOT EXISTS (SELECT 1 FROM custom_domains         WHERE organisation_id = o.id)
  AND NOT EXISTS (SELECT 1 FROM roadmaps               WHERE organisation_id = o.id);

COMMIT;

New signups still get personal organisations through the handle_new_user trigger. Re-run the membership/resource steps periodically if your fork keeps the shared-org model, or customise the trigger for your own deployment.

License

MIT. See LICENSE.

About

A 100% free forever platform to extend the customer facing capabilities of linear.app

Resources

Code of conduct

Stars

54 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages