feat(legal): public privacy policy, terms, and homepage for OAuth verification

Adds login-free /privacy and /terms pages (rendered outside the authenticated
tree via a pathname switch in main.tsx) carrying the Google API Services Limited
Use disclosure, YouTube ToS / Google Privacy links, and contact + data-deletion
info. The sign-in screen now describes the app and links to both, satisfying
Google's homepage + privacy-policy requirements for the OAuth consent screen.
This commit is contained in:
npeter83 2026-06-14 01:29:44 +02:00
parent dcaac097ec
commit b040381d02
5 changed files with 245 additions and 6 deletions

View file

@ -35,9 +35,9 @@ export default function Login() {
Sub<span className="text-accent">feed</span>
</div>
<p className="text-muted my-5 leading-relaxed">
Your subscriptions, filtered your way.
<br />
Sign in with your Google account.
A self-hosted, filterable feed of your YouTube subscriptions sort, tag, and
tune out the noise. Sign in with Google to get started; YouTube access is an
optional, separate step you control.
</p>
<a
href="/auth/login"
@ -87,6 +87,10 @@ export default function Login() {
)}
</div>
</div>
<footer className="mt-5 flex gap-4 text-xs text-muted">
<a href="/privacy" className="hover:text-fg transition">Privacy Policy</a>
<a href="/terms" className="hover:text-fg transition">Terms of Service</a>
</footer>
</div>
);
}

View file

@ -0,0 +1,47 @@
// Shared shell for the public, login-free legal pages (/privacy, /terms). These must be
// reachable unauthenticated — Google's OAuth consent screen links to them and reviewers
// fetch them directly — so they render outside the authenticated App tree (see main.tsx).
export const CONTACT_EMAIL = "b1fr0stmailops@gmail.com";
export const LAST_UPDATED = "June 14, 2026";
export function LegalLink({ href, children }: { href: string; children: React.ReactNode }) {
return (
<a href={href} target="_blank" rel="noreferrer" className="text-accent hover:underline">
{children}
</a>
);
}
export default function LegalLayout({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-bg text-fg">
<div className="mx-auto w-[min(92vw,720px)] py-12">
<a href="/" className="inline-block text-2xl font-bold tracking-tight mb-8">
Sub<span className="text-accent">feed</span>
</a>
<div className="glass rounded-2xl p-8">
<h1 className="text-2xl font-semibold">{title}</h1>
<p className="text-xs text-muted mt-1 mb-6">Last updated: {LAST_UPDATED}</p>
<div className="space-y-5 text-sm leading-relaxed text-fg/90">{children}</div>
</div>
<footer className="mt-6 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted">
<a href="/" className="hover:text-fg transition">Home</a>
<a href="/privacy" className="hover:text-fg transition">Privacy Policy</a>
<a href="/terms" className="hover:text-fg transition">Terms of Service</a>
<a href={`mailto:${CONTACT_EMAIL}`} className="hover:text-fg transition">Contact</a>
</footer>
</div>
</div>
);
}
export function H2({ children }: { children: React.ReactNode }) {
return <h2 className="text-base font-semibold text-fg pt-2">{children}</h2>;
}

View file

@ -0,0 +1,111 @@
import LegalLayout, { CONTACT_EMAIL, H2, LegalLink } from "./LegalLayout";
export default function PrivacyPolicy() {
return (
<LegalLayout title="Privacy Policy">
<p>
Subfeed is a personal, non-commercial project operated by an individual in Hungary
("we", "us"). It lets you view and organize your own YouTube subscriptions as a
filterable feed. This policy explains what data Subfeed accesses, how it is used, and
the choices you have. By using Subfeed you agree to this policy.
</p>
<H2>What we access</H2>
<p>Subfeed only accesses data you explicitly authorize, in two separate steps:</p>
<ul className="list-disc pl-5 space-y-1">
<li>
<span className="text-fg font-medium">Sign-in (always):</span> your basic Google
profile name, email address, profile picture, and Google account identifier used
solely to create and identify your account.
</li>
<li>
<span className="text-fg font-medium">YouTube read (optional, opt-in):</span> with the{" "}
<code className="text-xs">youtube.readonly</code> scope, your list of YouTube
subscriptions and the public metadata of those channels and their videos (titles,
thumbnails, durations, view counts, publish dates), used to build your feed.
</li>
<li>
<span className="text-fg font-medium">YouTube manage (optional, opt-in):</span> with
the <code className="text-xs">youtube</code> scope, the ability to unsubscribe from a
channel on YouTube performed only when you click to do so.
</li>
</ul>
<p>
You can use Subfeed with sign-in alone, and grant or decline each YouTube permission
independently.
</p>
<H2>How we use it</H2>
<p>
Your data is used exclusively to provide Subfeed's features to you: building and
displaying your personalized subscription feed, and carrying out actions (such as
unsubscribing) that you initiate. We do not use it for advertising, profiling, or any
purpose unrelated to the app.
</p>
<H2>Limited Use disclosure</H2>
<p>
Subfeed's use and transfer of information received from Google APIs to any other app
will adhere to the{" "}
<LegalLink href="https://developers.google.com/terms/api-services-user-data-policy">
Google API Services User Data Policy
</LegalLink>
, including the Limited Use requirements. We do not sell your data, do not transfer it
to third parties (except as needed to operate the app, e.g. the hosting infrastructure
we control), and do not use it for advertising.
</p>
<H2>Storage &amp; security</H2>
<p>
Data is stored in a private PostgreSQL database on a server we control, accessible only
to the operator. Google OAuth refresh tokens are encrypted at rest. Access is restricted
to the emails explicitly invited to the app. No system is perfectly secure, but we take
reasonable measures to protect your data.
</p>
<H2>Retention &amp; deletion</H2>
<p>
We keep your data while your account is active. You can revoke Subfeed's access to your
Google data at any time at{" "}
<LegalLink href="https://myaccount.google.com/permissions">
myaccount.google.com/permissions
</LegalLink>
; once revoked, the stored tokens can no longer be used. To have your account and
associated data deleted, email{" "}
<a href={`mailto:${CONTACT_EMAIL}`} className="text-accent hover:underline">
{CONTACT_EMAIL}
</a>
.
</p>
<H2>Cookies</H2>
<p>
Subfeed sets a single, signed session cookie to keep you logged in. It is not used for
tracking or advertising, and there are no third-party analytics cookies.
</p>
<H2>YouTube &amp; Google</H2>
<p>
Subfeed uses YouTube API Services. By using Subfeed you also agree to the{" "}
<LegalLink href="https://www.youtube.com/t/terms">YouTube Terms of Service</LegalLink>,
and your use of Google data is subject to the{" "}
<LegalLink href="https://policies.google.com/privacy">Google Privacy Policy</LegalLink>.
</p>
<H2>Changes</H2>
<p>
We may update this policy; material changes will be reflected by the "Last updated" date
above. Continued use after a change constitutes acceptance.
</p>
<H2>Contact</H2>
<p>
Questions about this policy or your data:{" "}
<a href={`mailto:${CONTACT_EMAIL}`} className="text-accent hover:underline">
{CONTACT_EMAIL}
</a>
.
</p>
</LegalLayout>
);
}

View file

@ -0,0 +1,67 @@
import LegalLayout, { CONTACT_EMAIL, H2, LegalLink } from "./LegalLayout";
export default function Terms() {
return (
<LegalLayout title="Terms of Service">
<p>
Subfeed is a personal, non-commercial project operated by an individual in Hungary. It
provides a filterable view of your own YouTube subscriptions. By accessing or using
Subfeed, you agree to these Terms. If you do not agree, do not use the service.
</p>
<H2>The service</H2>
<p>
Subfeed is offered on an invite-only basis, free of charge, and "as is", without
warranties of any kind. It is a hobby project: features may change, and the service may
be slowed, interrupted, or discontinued at any time without notice.
</p>
<H2>Your account &amp; acceptable use</H2>
<p>
You sign in with your Google account and may only access Subfeed with an email that has
been invited. You agree to use the service lawfully, not to abuse, disrupt, probe, or
overload it, and not to attempt to access other users' data. We may suspend or revoke
access at our discretion, for example in case of misuse.
</p>
<H2>Third-party services</H2>
<p>
Subfeed uses YouTube API Services. By using Subfeed you agree to be bound by the{" "}
<LegalLink href="https://www.youtube.com/t/terms">YouTube Terms of Service</LegalLink>.
Your data is handled as described in our{" "}
<a href="/privacy" className="text-accent hover:underline">Privacy Policy</a> and, for
Google data, the{" "}
<LegalLink href="https://policies.google.com/privacy">Google Privacy Policy</LegalLink>.
</p>
<H2>No warranty &amp; limitation of liability</H2>
<p>
To the maximum extent permitted by law, the service is provided without warranty, and
the operator is not liable for any indirect, incidental, or consequential damages, or
for any loss of data, arising from your use of Subfeed. Your sole remedy if you are
dissatisfied is to stop using the service and revoke its access to your account.
</p>
<H2>Governing law</H2>
<p>
These Terms are governed by the laws of Hungary and applicable European Union law,
without regard to conflict-of-law rules.
</p>
<H2>Changes</H2>
<p>
We may update these Terms; the "Last updated" date above reflects the latest version.
Continued use after a change constitutes acceptance.
</p>
<H2>Contact</H2>
<p>
Questions:{" "}
<a href={`mailto:${CONTACT_EMAIL}`} className="text-accent hover:underline">
{CONTACT_EMAIL}
</a>
.
</p>
</LegalLayout>
);
}