Reapply "FEATURE (ssr): Migrate to NextJS"

This reverts commit 042e10c49c.
This commit is contained in:
Rostislav Dugin
2025-11-09 17:54:13 +03:00
parent 042e10c49c
commit bb9cdc5ffc
89 changed files with 10077 additions and 6216 deletions

48
.gitignore vendored
View File

@@ -1,7 +1,41 @@
backend/
frontend/
.env
pgdata/
docker-compose.yml
postgresus-data/
node_modules/
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -1,9 +0,0 @@
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 100,
tabWidth: 2,
arrowParens: 'always',
plugins: ['prettier-plugin-tailwindcss'],
};

237
404.html
View File

@@ -1,237 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<!-- Primary Meta Tags -->
<title>404 - Page Not Found | Postgresus</title>
<meta name="title" content="404 - Page Not Found | Postgresus" />
<meta
name="description"
content="The page you're looking for doesn't exist. Return to Postgresus - PostgreSQL monitoring and backup tool."
/>
<meta name="robots" content="noindex, nofollow" />
<link rel="canonical" href="https://postgresus.com/404" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://postgresus.com/404" />
<meta property="og:title" content="404 - Page Not Found | Postgresus" />
<meta
property="og:description"
content="The page you're looking for doesn't exist. Return to Postgresus - PostgreSQL monitoring and backup tool."
/>
<meta property="og:image" content="https://postgresus.com/logo.svg" />
<!-- Twitter -->
<meta property="twitter:card" content="summary" />
<meta property="twitter:url" content="https://postgresus.com/404" />
<meta property="twitter:title" content="404 - Page Not Found | Postgresus" />
<meta
property="twitter:description"
content="The page you're looking for doesn't exist. Return to Postgresus - PostgreSQL monitoring and backup tool."
/>
<meta property="twitter:image" content="https://postgresus.com/logo.svg" />
<!-- Favicons -->
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="apple-touch-icon" href="favicon.svg" />
<link rel="shortcut icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Jost:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="index.css" />
<script async src="https://www.googletagmanager.com/gtag/js?id=G-GE01THYR9X"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-GE01THYR9X');
</script>
</head>
<body class="bg-gray-50">
<!------------------ NAVBAR ------------------------->
<nav
class="fixed left-0 right-0 top-0 z-50 flex h-[60px] justify-center bg-white px-4 shadow-sm sm:h-[70px] md:h-[80px]"
>
<div
class="flex w-[320px] min-w-0 max-w-[320px] grow items-center sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<a href="/" class="ml-2 text-lg font-bold sm:ml-3 sm:text-xl md:ml-4 md:text-2xl"
>Postgresus</a
>
<a
class="ml-auto"
href="https://github.com/RostislavDugin/postgresus"
target="_blank"
rel="noopener noreferrer"
>
<div
class="flex items-center rounded-lg border bg-[#f5f7f9] px-2 py-1 hover:bg-gray-100 md:px-4 md:py-2"
>
<svg
class="mr-1 sm:mr-2 md:mr-3"
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
/>
</svg>
<span class="text-sm sm:text-base">Star on GitHub</span>
</div>
</a>
</div>
</nav>
<!------------------ END OF NAVBAR ------------------>
<!------------------ 404 ERROR SECTION ------------------------->
<div class="flex min-h-screen items-center justify-center px-4 pt-[80px] md:pt-[100px]">
<div class="text-center">
<div class="mb-8">
<h1 class="mb-4 text-9xl font-bold text-blue-600">404</h1>
<h2 class="mb-4 text-2xl font-bold text-gray-800 sm:text-3xl md:text-4xl">
Page Not Found
</h2>
<p class="mx-auto max-w-md text-lg text-gray-600 sm:text-xl">
The page you're looking for doesn't exist or has been moved.
</p>
</div>
<div
class="flex flex-col items-center space-y-4 sm:flex-row sm:justify-center sm:space-x-4 sm:space-y-0"
>
<a
href="/"
class="inline-flex items-center rounded-lg border-2 border-blue-600 bg-blue-600 px-6 py-3 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
></path>
</svg>
Go Home
</a>
</div>
<div class="mt-8">
<p class="text-gray-500">
Need help? Visit our
<a
href="https://t.me/postgresus_community"
target="_blank"
class="text-blue-600 hover:underline"
>
Community
</a>
or check out our
<a
href="https://github.com/RostislavDugin/postgresus"
target="_blank"
class="text-blue-600 hover:underline"
>
GitHub repository
</a>
</p>
</div>
</div>
</div>
<!------------------ END OF 404 ERROR SECTION ------------------>
<!------------------ FOOTER SECTION -------------------------------->
<div class="flex justify-center bg-blue-600 py-[50px]">
<div
class="w-[320px] max-w-[320px] sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<div class="flex flex-col items-center space-y-4">
<div class="flex flex-wrap justify-center gap-6 text-white">
<a
href="https://github.com/RostislavDugin/postgresus"
target="_blank"
class="transition-colors hover:text-blue-200"
>
GitHub
</a>
<a
href="https://t.me/postgresus_community"
target="_blank"
class="transition-colors hover:text-blue-200"
>
Community
</a>
<a
href="https://rostislav-dugin.com"
target="_blank"
class="transition-colors hover:text-blue-200"
>
Developer
</a>
</div>
<p class="text-center text-sm text-white">&copy; 2025 Postgresus. All rights reserved.</p>
</div>
</div>
</div>
<!------------------ END OF FOOTER SECTION ------------------------->
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function (m, e, t, r, i, k, a) {
m[i] =
m[i] ||
function () {
(m[i].a = m[i].a || []).push(arguments);
};
m[i].l = 1 * new Date();
for (var j = 0; j < document.scripts.length; j++) {
if (document.scripts[j].src === r) {
return;
}
}
((k = e.createElement(t)),
(a = e.getElementsByTagName(t)[0]),
(k.async = 1),
(k.src = r),
a.parentNode.insertBefore(k, a));
})(window, document, 'script', 'https://mc.yandex.ru/metrika/tag.js?id=103482608', 'ym');
ym(103482608, 'init', {
ssr: true,
clickmap: true,
ecommerce: 'dataLayer',
accurateTrackBounce: true,
trackLinks: true,
});
</script>
<noscript
><div>
<img
src="https://mc.yandex.ru/watch/103482608"
style="position: absolute; left: -9999px"
alt=""
/></div
></noscript>
<!-- /Yandex.Metrika counter -->
</body>
</html>

View File

@@ -1,40 +1,36 @@
# Postgresus Website
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## CSS Development Workflow
## Getting Started
This project uses TailwindCSS. **You must build CSS before committing.**
### Setup (one time)
First, run the development server:
```bash
npm install
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
### Development Process
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
1. **Make changes** to `index.html` or other HTML files
2. **Build CSS** before committing:
```bash
npm run build-css
```
3. **Commit both files**:
```bash
git add .
git commit -m "Your changes"
git push
```
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
### For Active Development
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
```bash
# Watch for changes (rebuilds automatically)
npm run watch-css
```
## Learn More
### Important Files
To learn more about Next.js, take a look at the following resources:
- `src/input.css` - Source CSS (edit this)
- `styles.min.css` - Generated CSS (don't edit manually)
- `index.html` - Uses the generated CSS
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
**Always run `npm run build-css` before pushing to GitHub!**
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@@ -0,0 +1,59 @@
"use client";
import { useState } from "react";
interface CopyButtonProps {
text: string;
className?: string;
}
export function CopyButton({ text, className = "" }: CopyButtonProps) {
const [copied, setCopied] = useState(false);
const copyToClipboard = () => {
if (!navigator.clipboard) {
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
showCopyFeedback();
return;
}
navigator.clipboard
.writeText(text)
.then(() => {
showCopyFeedback();
})
.catch((err) => {
console.error("Failed to copy text: ", err);
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
showCopyFeedback();
});
};
const showCopyFeedback = () => {
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 2000);
};
return (
<button
onClick={copyToClipboard}
className={`rounded px-2 py-1 text-xs text-white transition-colors ${
copied ? "bg-green-500" : "bg-blue-600 hover:bg-blue-700"
} ${className}`}
>
{copied ? "Copied!" : "Copy"}
</button>
);
}

View File

@@ -0,0 +1,100 @@
"use client";
import { useEffect, useState, useRef } from "react";
interface Heading {
id: string;
text: string;
level: number;
}
export default function DocTableOfContentComponent() {
const [headings, setHeadings] = useState<Heading[]>([]);
const [activeId, setActiveId] = useState<string>("");
const observerRef = useRef<IntersectionObserver | null>(null);
useEffect(() => {
// Use a timeout to avoid cascading renders
const timeoutId = setTimeout(() => {
// Get all h1, h2, h3 headings from the page
const elements = Array.from(
document.querySelectorAll("article h1, article h2, article h3")
);
if (elements.length === 0) return;
const headingData: Heading[] = elements.map((element) => ({
id: element.id,
text: element.textContent || "",
level: parseInt(element.tagName.substring(1)),
}));
setHeadings(headingData);
// Set up intersection observer to track active heading
observerRef.current = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id);
}
});
},
{ rootMargin: "-100px 0px -80% 0px" }
);
elements.forEach((element) => {
observerRef.current?.observe(element);
});
}, 0);
return () => {
clearTimeout(timeoutId);
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, []);
const handleClick = (id: string) => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
};
if (headings.length === 0) {
return null;
}
return (
<aside className="hidden w-64 border-l border-gray-200 bg-white xl:block">
<div className="sticky top-0 h-screen overflow-y-auto p-6">
<h3 className="mb-4 text-sm font-semibold text-gray-900">
On This Page
</h3>
<nav>
<ul className="space-y-2 text-sm">
{headings.map((heading) => (
<li
key={heading.id}
style={{ paddingLeft: `${(heading.level - 1) * 0.75}rem` }}
>
<button
onClick={() => handleClick(heading.id)}
className={`block w-full text-left transition-colors cursor-pointer hover:text-blue-600 ${
activeId === heading.id
? "font-medium text-blue-600"
: "text-gray-600"
}`}
>
{heading.text}
</button>
</li>
))}
</ul>
</nav>
</div>
</aside>
);
}

View File

@@ -0,0 +1,62 @@
"use client";
import Image from "next/image";
import Link from "next/link";
export default function DocsNavbarComponent() {
return (
<nav className="flex h-[60px] w-full justify-center border-b border-gray-200 bg-white sm:h-[70px] md:h-[80px]">
<div className="flex min-w-0 grow items-center px-4 sm:px-6 md:px-10">
<Link href="/" className="flex items-center">
<Image
src="/logo.svg"
alt="Postgresus logo"
width={30}
height={30}
className="shrink-0 sm:h-[40px] sm:w-[40px] md:h-[50px] md:w-[50px]"
priority
/>
<div className="ml-2 select-none text-lg font-bold sm:ml-3 sm:text-xl md:ml-4 md:text-2xl">
Postgresus
</div>
</Link>
<div className="ml-auto mr-4 hidden gap-3 sm:mr-6 md:mr-10 lg:flex lg:gap-5">
<a
className="hover:opacity-70"
href="https://t.me/postgresus_community"
target="_blank"
rel="noopener noreferrer"
>
Community
</a>
</div>
<a
className="ml-auto lg:ml-0"
href="https://github.com/RostislavDugin/postgresus"
target="_blank"
rel="noopener noreferrer"
>
<div className="flex items-center rounded-lg border border-gray-200 bg-[#f5f7f9] px-2 py-1 hover:bg-gray-100 md:px-4 md:py-2">
<Image
src="/images/index/github.svg"
className="mr-1 h-4 w-4 sm:mr-2 md:mr-3"
alt="GitHub icon"
width={16}
height={16}
priority
/>
<span className="text-sm sm:text-base">
Star on GitHub
<span className="hidden sm:inline">
, it&apos;s really important
</span>
</span>
</div>
</a>
</div>
</nav>
);
}

View File

@@ -0,0 +1,261 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState, useEffect } from "react";
interface NavItem {
title: string;
href: string;
children?: NavItem[];
}
const navItems: NavItem[] = [
{
title: "Installation",
href: "/installation",
},
{
title: "Storages",
href: "/storages",
children: [
{ title: "Google Drive", href: "/storages/google-drive" },
{ title: "Cloudflare R2", href: "/storages/cloudflare-r2" },
],
},
{
title: "Notifiers",
href: "/notifiers",
children: [
{ title: "Slack", href: "/notifiers/slack" },
{ title: "Microsoft Teams", href: "/notifiers/teams" },
],
},
{
title: "Reset password",
href: "/password",
},
];
export default function DocsSidebarComponent() {
const pathname = usePathname();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [manuallyToggledSections, setManuallyToggledSections] = useState<
Set<string>
>(new Set());
const isActive = (href: string) => {
return pathname === href;
};
const isParentActive = (item: NavItem) => {
if (item.children) {
return item.children.some((child) => pathname === child.href);
}
return false;
};
// Determine if a section should be expanded
const isSectionExpanded = (href: string) => {
// If manually toggled, respect that
if (manuallyToggledSections.has(href)) {
return true;
}
// Auto-expand if a child page is active
const item = navItems.find((i) => i.href === href);
if (item && isParentActive(item)) {
return true;
}
return false;
};
// Manage body overflow when mobile menu is open
useEffect(() => {
if (isMobileMenuOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
// Cleanup on unmount
return () => {
document.body.style.overflow = "";
};
}, [isMobileMenuOpen]);
const toggleSection = (href: string, hasChildren: boolean) => {
if (!hasChildren) return;
setManuallyToggledSections((prev) => {
const newSet = new Set(prev);
if (newSet.has(href)) {
newSet.delete(href);
} else {
newSet.add(href);
}
return newSet;
});
};
const renderSidebarContent = () => (
<nav className="space-y-1">
{navItems.map((item) => (
<div key={item.href}>
<div className="flex items-center">
<Link
href={item.href}
onClick={() => setIsMobileMenuOpen(false)}
className={`flex-1 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
isActive(item.href)
? "bg-blue-50 text-blue-700"
: "text-gray-700 hover:bg-gray-100 hover:text-gray-900"
}`}
>
{item.title}
</Link>
{item.children && (
<button
onClick={() => toggleSection(item.href, !!item.children)}
className={`ml-1 rounded-lg p-2 transition-all duration-200 ${
isActive(item.href)
? "text-blue-700 hover:bg-blue-100"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900"
}`}
aria-label={`Toggle ${item.title} section`}
>
<svg
className={`h-4 w-4 transition-transform duration-200 ${
isSectionExpanded(item.href) ? "rotate-180" : ""
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
)}
</div>
{item.children && (
<div
className={`overflow-hidden transition-all duration-300 ease-in-out ${
isSectionExpanded(item.href)
? "max-h-96 opacity-100"
: "max-h-0 opacity-0"
}`}
>
<div className="ml-4 mt-1 space-y-1 border-l-2 border-gray-200 pl-3">
{item.children.map((child) => (
<Link
key={child.href}
href={child.href}
onClick={() => setIsMobileMenuOpen(false)}
className={`block rounded-lg px-3 py-2 text-sm transition-colors ${
isActive(child.href)
? "bg-blue-50 text-blue-700 font-medium"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
}`}
>
{child.title}
</Link>
))}
</div>
</div>
)}
</div>
))}
</nav>
);
return (
<>
{/* Mobile Menu Button */}
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="fixed bottom-4 right-4 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg hover:bg-blue-700 lg:hidden"
aria-label="Toggle navigation menu"
>
{isMobileMenuOpen ? (
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
) : (
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
)}
</button>
{/* Mobile Menu Overlay */}
{isMobileMenuOpen && (
<div
className="fixed inset-0 z-40 backdrop-blur-md bg-white/10 lg:hidden"
onClick={() => setIsMobileMenuOpen(false)}
/>
)}
{/* Mobile Menu */}
<aside
className={`fixed bottom-0 left-0 right-0 z-40 max-h-[80vh] overflow-y-auto rounded-t-2xl border-t border-gray-200 bg-white p-6 shadow-2xl transition-transform duration-300 lg:hidden ${
isMobileMenuOpen ? "translate-y-0" : "translate-y-full"
}`}
>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">Navigation</h2>
<button
onClick={() => setIsMobileMenuOpen(false)}
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100"
aria-label="Close menu"
>
<svg
className="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{renderSidebarContent()}
</aside>
{/* Desktop Sidebar */}
<aside className="hidden w-64 border-r border-gray-200 bg-white lg:block">
<div className="sticky top-0 h-screen overflow-y-auto p-6">
{renderSidebarContent()}
</div>
</aside>
</>
);
}

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

146
app/globals.css Normal file
View File

@@ -0,0 +1,146 @@
@import "tailwindcss";
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-jost);
}
body {
background: var(--background);
color: var(--foreground);
font-family: Jost, Arial, Helvetica, sans-serif;
line-height: 1.6;
}
/* Article/prose context headings - for documentation pages */
article h1 {
font-size: 2.75rem;
font-weight: 800;
color: #1a202c;
border-bottom: 2px solid #3b82f6;
padding-bottom: 0.5rem;
margin-bottom: 2rem;
letter-spacing: -0.03em;
}
article h2 {
font-size: 1.875rem;
font-weight: 700;
color: #2563eb;
margin-top: 3rem;
margin-bottom: 1.5rem;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
}
article h3 {
font-size: 1.5rem;
font-weight: 600;
color: #374151;
margin-top: 2rem;
margin-bottom: 1rem;
}
article a {
color: #3b82f6;
text-decoration: underline;
font-weight: 500;
}
article p {
margin-bottom: 1rem;
}
article ul {
margin-bottom: 1.5rem;
padding-left: 1.5rem;
list-style-type: disc;
}
article ol {
margin-bottom: 1.5rem;
padding-left: 1.5rem;
list-style-type: decimal;
}
article li {
margin-bottom: 0.5rem;
}
article code {
background-color: #f3f4f6;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.875em;
color: #1f2937;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
}
article pre {
margin-bottom: 1.5rem;
overflow-x: auto;
max-width: 100%;
-webkit-overflow-scrolling: touch;
}
article pre code {
background-color: transparent;
padding: 0;
font-size: 0.875rem;
color: inherit;
display: block;
white-space: pre;
word-wrap: normal;
overflow-wrap: normal;
}
@media (max-width: 768px) {
article h1 {
font-size: 1.75rem;
margin-bottom: 1.5rem;
}
article h2 {
font-size: 1.5rem;
margin-top: 2rem;
margin-bottom: 1rem;
}
article h3 {
font-size: 1.25rem;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
article ul,
article ol {
padding-left: 1.25rem;
}
article .relative {
margin-left: -1rem;
margin-right: -1rem;
width: calc(100vw - 3rem);
max-width: calc(100vw - 3rem);
}
article pre {
font-size: 0.75rem;
padding: 0.75rem !important;
margin-left: 0;
margin-right: 0;
border-radius: 0.5rem;
}
article pre code {
font-size: 0.75rem;
line-height: 1.5;
}
article .relative {
margin-left: 0;
margin-right: 0;
}
}

78
app/gone/route.ts Normal file
View File

@@ -0,0 +1,78 @@
import { NextResponse } from "next/server";
export async function GET() {
return new NextResponse(
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>410 - Content Removed</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #f9fafb;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
}
.container {
max-width: 28rem;
text-align: center;
}
h1 {
font-size: 4rem;
font-weight: bold;
color: #111827;
margin-bottom: 1rem;
}
h2 {
font-size: 1.5rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 1rem;
}
p {
color: #4b5563;
margin-bottom: 2rem;
line-height: 1.6;
}
a {
display: inline-block;
background-color: #2563eb;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
text-decoration: none;
transition: background-color 0.2s;
}
a:hover {
background-color: #1d4ed8;
}
</style>
</head>
<body>
<div class="container">
<h1>410</h1>
<h2>Content Permanently Removed</h2>
<p>The content you are looking for has been permanently removed and is no longer available.</p>
<a href="/">Return to Home</a>
</div>
</body>
</html>`,
{
status: 410,
headers: {
"Content-Type": "text/html; charset=utf-8",
},
}
);
}

369
app/installation/page.tsx Normal file
View File

@@ -0,0 +1,369 @@
import type { Metadata } from "next";
import { CopyButton } from "../components/CopyButton";
import DocsNavbarComponent from "../components/DocsNavbarComponent";
import DocsSidebarComponent from "../components/DocsSidebarComponent";
import DocTableOfContentComponent from "../components/DocTableOfContentComponent";
export const metadata: Metadata = {
title: "Installation - Postgresus Documentation",
description:
"Learn how to install Postgresus using automated script, Docker run or Docker Compose. Simple zero-config installation for your self-hosted PostgreSQL backup system.",
keywords: [
"Postgresus installation",
"Docker installation",
"PostgreSQL backup setup",
"self-hosted backup",
"Docker Compose",
"database backup installation",
"pg_dump setup",
],
openGraph: {
title: "Installation - Postgresus Documentation",
description:
"Learn how to install Postgresus using automated script, Docker run or Docker Compose. Simple zero-config installation for your self-hosted PostgreSQL backup system.",
type: "article",
url: "https://postgresus.com/installation",
},
twitter: {
card: "summary",
title: "Installation - Postgresus Documentation",
description:
"Learn how to install Postgresus using automated script, Docker run or Docker Compose. Simple zero-config installation for your self-hosted PostgreSQL backup system.",
},
alternates: {
canonical: "https://postgresus.com/installation",
},
robots: "index, follow",
};
export default function InstallationPage() {
const installScript = `sudo apt-get install -y curl && \\
sudo curl -sSL https://raw.githubusercontent.com/RostislavDugin/postgresus/refs/heads/main/install-postgresus.sh | sudo bash`;
const dockerRun = `docker run -d \\
--name postgresus \\
-p 4005:4005 \\
-v ./postgresus-data:/postgresus-data \\
--restart unless-stopped \\
rostislavdugin/postgresus:latest`;
const dockerCompose = `services:
postgresus:
container_name: postgresus
image: rostislavdugin/postgresus:latest
ports:
- "4005:4005"
volumes:
- ./postgresus-data:/postgresus-data
restart: unless-stopped`;
return (
<>
{/* JSON-LD Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "TechArticle",
headline: "Installation - Postgresus Documentation",
description:
"Learn how to install Postgresus using automated script, Docker run or Docker Compose. Simple zero-config installation for your self-hosted PostgreSQL backup system.",
author: {
"@type": "Organization",
name: "Postgresus",
},
publisher: {
"@type": "Organization",
name: "Postgresus",
logo: {
"@type": "ImageObject",
url: "https://postgresus.com/logo.svg",
},
},
datePublished: "2025-01-01",
dateModified: "2025-01-01",
}),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "HowTo",
name: "How to install Postgresus",
description:
"Step-by-step guide to install Postgresus PostgreSQL backup tool",
step: [
{
"@type": "HowToStep",
name: "Automated installation script",
text: "Run the automated installation script to install Docker and set up Postgresus with automatic startup configuration.",
itemListElement: [
{
"@type": "HowToDirection",
text: "Execute the curl command to download and run the installation script",
},
],
},
{
"@type": "HowToStep",
name: "Docker Run",
text: "Use Docker run command to quickly start Postgresus container with data persistence.",
},
{
"@type": "HowToStep",
name: "Docker Compose",
text: "Create a docker-compose.yml file and use Docker Compose for managed deployment.",
},
],
}),
}}
/>
<DocsNavbarComponent />
<div className="flex min-h-screen">
{/* Sidebar */}
<DocsSidebarComponent />
{/* Main Content */}
<main className="flex-1 px-4 py-6 sm:px-6 sm:py-8 lg:px-12">
<div className="mx-auto max-w-4xl">
<article className="prose prose-blue max-w-none">
<h1 id="installation">Installation</h1>
<p className="text-lg text-gray-700">
You have three ways to install Postgresus: automated script
(recommended), simple Docker run or Docker Compose setup.
</p>
<h2 id="system-requirements">System requirements</h2>
<p>
Postgresus requires the following minimum system resources to
run properly:
</p>
<ul>
<li>
<strong>CPU</strong>: At least 1 CPU cores
</li>
<li>
<strong>RAM</strong>: Minimum 500 MB RAM
</li>
<li>
<strong>Storage</strong>: 5 GB for installation and as much as
you need for backups
</li>
<li>
<strong>Docker</strong>: Docker Engine 20.10+ and Docker
Compose v2.0+
</li>
</ul>
<h2 id="option-1-automated-script">
Option 1: installation script (recommended, Linux only)
</h2>
<p>The installation script will:</p>
<ul>
<li>
Install Docker with Docker Compose (if not already
installed)
</li>
<li> Set up Postgresus</li>
<li> Configure automatic startup on system reboot</li>
</ul>
<div className="relative my-6">
<pre className="overflow-x-auto rounded-lg bg-gray-900 p-4 text-sm text-gray-100">
<code>{installScript}</code>
</pre>
<div className="absolute right-2 top-2">
<CopyButton text={installScript} />
</div>
</div>
<p>
In this case Postgresus will be installed in{" "}
<code>/opt/postgresus</code> directory.
</p>
<h2 id="option-2-docker-run">Option 2: Simple Docker run</h2>
<p>The easiest way to run Postgresus:</p>
<div className="relative my-6">
<pre className="overflow-x-auto rounded-lg bg-gray-900 p-4 text-sm text-gray-100">
<code>{dockerRun}</code>
</pre>
<div className="absolute right-2 top-2">
<CopyButton text={dockerRun} />
</div>
</div>
<p>This single command will:</p>
<ul>
<li> Start Postgresus</li>
<li>
Store all data in <code>./postgresus-data</code> directory
</li>
<li> Automatically restart on system reboot</li>
</ul>
<h2 id="option-3-docker-compose">
Option 3: Docker Compose setup
</h2>
<p>
Create a <code>docker-compose.yml</code> file with the following
configuration:
</p>
<div className="relative my-6">
<pre className="overflow-x-auto rounded-lg bg-gray-900 p-4 text-sm text-gray-100">
<code>{dockerCompose}</code>
</pre>
<div className="absolute right-2 top-2">
<CopyButton text={dockerCompose} />
</div>
</div>
<p>Then run:</p>
<div className="relative my-6">
<pre className="overflow-x-auto rounded-lg bg-gray-900 p-4 text-sm text-gray-100">
<code>docker compose up -d</code>
</pre>
<div className="absolute right-2 top-2">
<CopyButton text="docker compose up -d" />
</div>
</div>
<p>Keep in mind that start up can take up to ~2 minutes.</p>
<h2 id="getting-started">Getting started</h2>
<p>After installation:</p>
<ol>
<li>
<strong>Launch and access Postgresus</strong>: Start
Postgresus and navigate to <code>http://localhost:4005</code>
</li>
<li>
<strong>Create your first backup job</strong>: Click &quot;New
Backup&quot; and configure your PostgreSQL database connection
</li>
<li>
<strong>Configure schedule</strong>: Set up your backup
schedule (hourly, daily, weekly or monthly)
</li>
<li>
<strong>Choose storage destination</strong>: Select where to
store your backups (local, S3, Google Drive, etc.)
</li>
<li>
<strong>Set up notifications</strong>: Add notification
channels (Slack, Telegram, Discord) to get alerts about backup
status
</li>
<li>
<strong>Start backing up</strong>: Save your configuration and
watch your first backup run!
</li>
</ol>
<h2 id="how-to-update">How to update Postgresus?</h2>
<p>
To update Postgresus, you need to stop it, clean up Docker cache
and restart the container.
</p>
<ol>
<li>
Go to the directory where Postgresus is installed (usually{" "}
<code>/opt/postgresus</code>)
</li>
<li>
Stop the container: <code>docker compose stop</code>
</li>
<li>
Clean up Docker cache: <code>docker system prune -a</code>
</li>
<li>
Restart the container: <code>docker compose up -d</code>
</li>
</ol>
<p>
It will get the latest version of Postgresus from the Docker Hub
(if you have not fixed the version in the{" "}
<code>docker-compose.yml</code> file).
</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<h3 id="container-wont-start">Container won&apos;t start</h3>
<p>If the container fails to start, check the logs:</p>
<div className="relative my-6">
<pre className="overflow-x-auto rounded-lg bg-gray-900 p-4 text-sm text-gray-100">
<code>docker logs postgresus</code>
</pre>
<div className="absolute right-2 top-2">
<CopyButton text="docker logs postgresus" />
</div>
</div>
<h3 id="port-already-in-use">Port already in use</h3>
<p>
If port 4005 is already in use, you can change it in your
docker-compose.yml:
</p>
<div className="relative my-6">
<pre className="overflow-x-auto rounded-lg bg-gray-900 p-4 text-sm text-gray-100">
<code>
ports:
{"\n "}- &quot;8080:4005&quot; # Change 8080 to any
available port
</code>
</pre>
</div>
<h3 id="permission-denied">Permission denied errors</h3>
<p>If you encounter permission issues with the data directory:</p>
<div className="relative my-6">
<pre className="overflow-x-auto rounded-lg bg-gray-900 p-4 text-sm text-gray-100">
<code>
sudo chown -R $USER:$USER ./postgresus-data
{"\n"}
chmod -R 755 ./postgresus-data
</code>
</pre>
<div className="absolute right-2 top-2">
<CopyButton
text={`sudo chown -R $USER:$USER ./postgresus-data\nchmod -R 755 ./postgresus-data`}
/>
</div>
</div>
</article>
</div>
</main>
{/* Table of Contents */}
<DocTableOfContentComponent />
</div>
</>
);
}

55
app/layout.tsx Normal file
View File

@@ -0,0 +1,55 @@
import { Jost } from "next/font/google";
import "./globals.css";
import Script from "next/script";
const jost = Jost({
weight: ["400", "500", "600", "700", "800"],
style: ["normal"],
subsets: ["latin"],
display: "swap",
});
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={jost.className}
style={{ fontFamily: "Jost, sans-serif" }}
>
{children}
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-GE01THYR9X"
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-GE01THYR9X');
`}
</Script>
<Script id="yandex-metrika" strategy="afterInteractive">
{`
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, 'script', 'https://mc.yandex.ru/metrika/tag.js?id=103482608', 'ym');
ym(103482608, 'init', {
ssr: true,
clickmap: true,
ecommerce: 'dataLayer',
accurateTrackBounce: true,
trackLinks: true,
});
`}
</Script>
</body>
</html>
);
}

112
app/not-found.tsx Normal file
View File

@@ -0,0 +1,112 @@
import type { Metadata } from "next";
import Link from "next/link";
import Image from "next/image";
export const metadata: Metadata = {
title: "404 - Page Not Found | Postgresus",
description: "The page you're looking for doesn't exist.",
robots: "noindex, nofollow",
};
export default function NotFound() {
return (
<div className="flex min-h-screen flex-col">
{/* Navbar */}
<nav className="flex h-[60px] w-full justify-center border-b border-gray-200 bg-white sm:h-[70px] md:h-[80px]">
<div className="flex min-w-0 grow items-center px-4 sm:px-6 md:px-10">
<Link href="/" className="flex items-center">
<Image
src="/logo.svg"
alt="Postgresus logo"
width={30}
height={30}
className="shrink-0 sm:h-[40px] sm:w-[40px] md:h-[50px] md:w-[50px]"
priority
/>
<div className="ml-2 select-none text-lg font-bold sm:ml-3 sm:text-xl md:ml-4 md:text-2xl">
Postgresus
</div>
</Link>
<div className="ml-auto mr-4 hidden gap-3 sm:mr-6 md:mr-10 lg:flex lg:gap-5">
<a className="hover:opacity-70" href="/docs">
Docs
</a>
<a
className="hover:opacity-70"
href="https://t.me/postgresus_community"
target="_blank"
rel="noopener noreferrer"
>
Community
</a>
</div>
<a
className="ml-auto lg:ml-0"
href="https://github.com/RostislavDugin/postgresus"
target="_blank"
rel="noopener noreferrer"
>
<div className="flex items-center rounded-lg border border-gray-200 bg-[#f5f7f9] px-2 py-1 hover:bg-gray-100 md:px-4 md:py-2">
<Image
src="/images/index/github.svg"
className="mr-1 h-4 w-4 sm:mr-2 md:mr-3"
alt="GitHub icon"
width={16}
height={16}
priority
/>
<span className="text-sm sm:text-base">
Star on GitHub
<span className="hidden sm:inline">
, it&apos;s really important
</span>
</span>
</div>
</a>
</div>
</nav>
{/* 404 Content */}
<div className="flex grow flex-col items-center justify-center bg-linear-to-b from-white to-gray-50 px-6 py-12 text-center">
<div className="mb-4">
<h1 className="text-7xl font-bold text-blue-600 md:text-8xl">404</h1>
</div>
<h2 className="mb-3 text-2xl font-bold text-gray-800 md:text-3xl">
Page Not Found
</h2>
<p className="mb-6 max-w-md text-base text-gray-600">
The page you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<div className="flex flex-col gap-3 sm:flex-row">
<Link
href="/"
className="rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-blue-700 md:px-6 md:py-3 md:text-base"
>
Go to Homepage
</Link>
<Link
href="/installation"
className="rounded-lg border-2 border-blue-600 bg-white px-5 py-2.5 text-sm font-semibold text-blue-600 transition-colors hover:bg-blue-50 md:px-6 md:py-3 md:text-base"
>
View Documentation
</Link>
</div>
</div>
{/* Footer */}
<footer className="border-t border-gray-200 bg-white py-8 text-center text-sm text-gray-600">
<p>
© {new Date().getFullYear()} Postgresus. Open source PostgreSQL backup
tool.
</p>
</footer>
</div>
);
}

136
app/notifiers/page.tsx Normal file
View File

@@ -0,0 +1,136 @@
import type { Metadata } from "next";
import Link from "next/link";
import DocsNavbarComponent from "../components/DocsNavbarComponent";
import DocsSidebarComponent from "../components/DocsSidebarComponent";
import DocTableOfContentComponent from "../components/DocTableOfContentComponent";
export const metadata: Metadata = {
title: "Notifiers - Postgresus Documentation",
description:
"List of supported notification channels for Postgresus backup alerts including Slack, Discord, Telegram, Microsoft Teams, Email and Webhooks.",
keywords: [
"Postgresus notifiers",
"backup notifications",
"Slack notifications",
"Discord alerts",
"Telegram notifications",
"Teams notifications",
"Email alerts",
"Webhook notifications",
],
openGraph: {
title: "Notifiers - Postgresus Documentation",
description:
"List of supported notification channels for Postgresus backup alerts including Slack, Discord, Telegram, Microsoft Teams, Email and Webhooks.",
type: "article",
url: "https://postgresus.com/notifiers",
},
twitter: {
card: "summary",
title: "Notifiers - Postgresus Documentation",
description:
"List of supported notification channels for Postgresus backup alerts including Slack, Discord, Telegram, Microsoft Teams, Email and Webhooks.",
},
alternates: {
canonical: "https://postgresus.com/notifiers",
},
robots: "index, follow",
};
export default function NotifiersPage() {
return (
<>
{/* JSON-LD Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "TechArticle",
headline: "Notifiers - Postgresus Documentation",
description:
"List of supported notification channels for Postgresus backup alerts including Slack, Discord, Telegram, Microsoft Teams, Email and Webhooks.",
author: {
"@type": "Organization",
name: "Postgresus",
},
publisher: {
"@type": "Organization",
name: "Postgresus",
logo: {
"@type": "ImageObject",
url: "https://postgresus.com/logo.svg",
},
},
datePublished: "2025-01-01",
dateModified: "2025-01-01",
}),
}}
/>
<DocsNavbarComponent />
<div className="flex min-h-screen">
{/* Sidebar */}
<DocsSidebarComponent />
{/* Main Content */}
<main className="flex-1 px-4 py-6 sm:px-6 sm:py-8 lg:px-12">
<div className="mx-auto max-w-4xl">
<article className="prose prose-blue max-w-none">
<h1 id="notifiers">Notifiers</h1>
<p className="text-lg text-gray-700">
Postgresus supports multiple notification channels to keep you
informed about your PostgreSQL backup status. Get instant alerts
when backups succeed, fail or encounter issues.
</p>
<h2 id="supported-notifiers">Supported notifiers</h2>
<ul>
<li>
<Link
href="/notifiers/slack"
className="font-semibold text-blue-600 hover:text-blue-800"
>
Slack
</Link>{" "}
- Send notifications to Slack channels via webhooks
</li>
<li>
<strong>Discord</strong> - Post backup alerts to Discord
channels
</li>
<li>
<strong>Telegram</strong> - Receive notifications through
Telegram bots
</li>
<li>
<Link
href="/notifiers/teams"
className="font-semibold text-blue-600 hover:text-blue-800"
>
Microsoft Teams
</Link>{" "}
- Notify your team via Microsoft Teams channels
</li>
<li>
<strong>Email</strong> - Send email notifications for backup
events
</li>
<li>
<strong>Webhook</strong> - Custom webhook integration for any
service
</li>
</ul>
</article>
</div>
</main>
{/* Table of Contents */}
<DocTableOfContentComponent />
</div>
</>
);
}

View File

@@ -0,0 +1,262 @@
import type { Metadata } from "next";
import DocsNavbarComponent from "../../components/DocsNavbarComponent";
import DocsSidebarComponent from "../../components/DocsSidebarComponent";
import DocTableOfContentComponent from "../../components/DocTableOfContentComponent";
import Image from "next/image";
export const metadata: Metadata = {
title: "How to configure Slack notifications for Postgresus | Postgresus",
description:
"Step-by-step guide to set up Slack notifications for PostgreSQL backup alerts with Postgresus. Learn how to create Slack webhook and configure notifications.",
keywords: [
"Postgresus",
"Slack notifications",
"PostgreSQL backup",
"Slack webhook",
"backup alerts",
"database notifications",
],
openGraph: {
title: "How to configure Slack notifications for Postgresus | Postgresus",
description:
"Step-by-step guide to set up Slack notifications for PostgreSQL backup alerts with Postgresus. Learn how to create Slack webhook and configure notifications.",
type: "article",
url: "https://postgresus.com/notifiers/slack",
},
twitter: {
card: "summary",
title: "How to configure Slack notifications for Postgresus | Postgresus",
description:
"Step-by-step guide to set up Slack notifications for PostgreSQL backup alerts with Postgresus. Learn how to create Slack webhook and configure notifications.",
},
alternates: {
canonical: "https://postgresus.com/notifiers/slack",
},
robots: "index, follow",
};
export default function SlackPage() {
return (
<>
{/* JSON-LD Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "HowTo",
name: "How to configure Slack notifications for Postgresus",
description:
"Step-by-step guide to set up Slack notifications for PostgreSQL backup alerts with Postgresus",
step: [
{
"@type": "HowToStep",
name: "Open Slack workspace",
text: "Navigate to your Slack workspace where you want to receive notifications.",
},
{
"@type": "HowToStep",
name: "Access Slack apps",
text: "Go to your Slack workspace and access the Apps section.",
},
{
"@type": "HowToStep",
name: "Create incoming webhook",
text: "Search for and add the Incoming Webhooks app to your workspace.",
},
{
"@type": "HowToStep",
name: "Select channel",
text: "Choose the channel where you want to receive backup notifications.",
},
{
"@type": "HowToStep",
name: "Copy webhook URL",
text: "Copy the generated webhook URL from Slack.",
},
{
"@type": "HowToStep",
name: "Configure in Postgresus",
text: "Paste the webhook URL into Postgresus notifier configuration.",
},
{
"@type": "HowToStep",
name: "Test the notification",
text: "Test the notification to ensure it's working correctly.",
},
],
}),
}}
/>
<DocsNavbarComponent />
<div className="flex min-h-screen">
{/* Sidebar */}
<DocsSidebarComponent />
{/* Main Content */}
<main className="flex-1 px-4 py-6 sm:px-6 sm:py-8 lg:px-12">
<div className="mx-auto max-w-4xl">
<article className="prose prose-blue max-w-none">
<h1 id="slack-notifications">Slack notifications</h1>
<p className="text-lg text-gray-700">
Configure Slack to receive instant notifications about your
PostgreSQL backup status. Get alerts for successful backups,
failures and warnings directly in your Slack channels.
</p>
<h2 id="setup-slack-webhook">Setup Slack webhook</h2>
<h3 id="open-slack-workspace">1. Open your Slack workspace</h3>
<p>
Navigate to your Slack workspace where you want to receive
backup notifications.
</p>
<Image
src="/images/notifier-slack/image-1.png"
alt="Open Slack workspace"
width={800}
height={500}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="access-slack-apps">2. Access Slack apps</h3>
<p>
Click on your workspace name in the top left, then select{" "}
<strong>&quot;Settings &amp; administration&quot;</strong> {" "}
<strong>&quot;Manage apps&quot;</strong>.
</p>
<Image
src="/images/notifier-slack/image-2.png"
alt="Access Slack Apps"
width={600}
height={400}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="search-incoming-webhooks">
3. Search for incoming webhooks
</h3>
<p>
In the App Directory, search for{" "}
<strong>&quot;Incoming Webhooks&quot;</strong> and click on it.
</p>
<Image
src="/images/notifier-slack/image-3.png"
alt="Search for Incoming Webhooks"
width={700}
height={400}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="add-to-slack">4. Add to Slack</h3>
<p>
Click the <strong>&quot;Add to Slack&quot;</strong> button to
install the Incoming Webhooks app to your workspace.
</p>
<Image
src="/images/notifier-slack/image-4.png"
alt="Add to Slack"
width={700}
height={400}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="select-channel">5. Select channel</h3>
<p>
Choose the channel where you want to receive backup
notifications, then click{" "}
<strong>&quot;Add Incoming Webhooks integration&quot;</strong>.
</p>
<Image
src="/images/notifier-slack/image-5.png"
alt="Select channel for notifications"
width={600}
height={400}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="copy-webhook-url">6. Copy webhook URL</h3>
<p>
After creating the webhook, you&apos;ll see the{" "}
<strong>Webhook URL</strong>. Copy this URL - you&apos;ll need
it for Postgresus configuration.
</p>
<Image
src="/images/notifier-slack/image-6.png"
alt="Copy webhook URL"
width={700}
height={500}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h2 id="configure-postgresus">Configure in Postgresus</h2>
<h3 id="add-slack-notifier">1. Add Slack notifier</h3>
<p>
In Postgresus, navigate to the notifiers settings and add a new
Slack notifier. Paste the webhook URL you copied from Slack.
</p>
<Image
src="/images/notifier-slack/image-7.png"
alt="Configure Slack in Postgresus"
width={600}
height={400}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="test-notification">2. Test the notification</h3>
<p>
After configuring the webhook, test the notification to ensure
it&apos;s working correctly. You should receive a test message
in your selected Slack channel.
</p>
<p>
That&apos;s it! Your Slack workspace is now configured to
receive PostgreSQL backup notifications from Postgresus.
</p>
{/* Navigation */}
<div className="mt-12 border-t border-gray-200 pt-8">
<a
href="/notifiers"
className="inline-flex items-center font-semibold text-blue-600 hover:text-blue-800"
>
Back to notifiers
</a>
</div>
</article>
</div>
</main>
{/* Table of Contents */}
<DocTableOfContentComponent />
</div>
</>
);
}

View File

@@ -0,0 +1,274 @@
import type { Metadata } from "next";
import DocsNavbarComponent from "../../components/DocsNavbarComponent";
import DocsSidebarComponent from "../../components/DocsSidebarComponent";
import DocTableOfContentComponent from "../../components/DocTableOfContentComponent";
import Image from "next/image";
export const metadata: Metadata = {
title:
"How to configure Microsoft Teams notifications for Postgresus | Postgresus",
description:
"Step-by-step guide to set up Microsoft Teams notifications for PostgreSQL backup alerts with Postgresus. Learn how to create Teams webhook and configure notifications.",
keywords: [
"Postgresus",
"Microsoft Teams notifications",
"PostgreSQL backup",
"Teams webhook",
"backup alerts",
"database notifications",
],
openGraph: {
title:
"How to configure Microsoft Teams notifications for Postgresus | Postgresus",
description:
"Step-by-step guide to set up Microsoft Teams notifications for PostgreSQL backup alerts with Postgresus. Learn how to create Teams webhook and configure notifications.",
type: "article",
url: "https://postgresus.com/notifiers/teams",
},
twitter: {
card: "summary",
title:
"How to configure Microsoft Teams notifications for Postgresus | Postgresus",
description:
"Step-by-step guide to set up Microsoft Teams notifications for PostgreSQL backup alerts with Postgresus. Learn how to create Teams webhook and configure notifications.",
},
alternates: {
canonical: "https://postgresus.com/notifiers/teams",
},
robots: "index, follow",
};
export default function TeamsPage() {
return (
<>
{/* JSON-LD Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "HowTo",
name: "How to configure Microsoft Teams notifications for Postgresus",
description:
"Step-by-step guide to set up Microsoft Teams notifications for PostgreSQL backup alerts with Postgresus",
step: [
{
"@type": "HowToStep",
name: "Open Teams channel",
text: "Navigate to the Microsoft Teams channel where you want to receive notifications.",
},
{
"@type": "HowToStep",
name: "Access workflows",
text: "Open the Workflows feature in your Teams channel.",
},
{
"@type": "HowToStep",
name: "Create new workflow",
text: "Create a new workflow for incoming webhooks.",
},
{
"@type": "HowToStep",
name: "Select webhook template",
text: "Choose the incoming webhook template from available options.",
},
{
"@type": "HowToStep",
name: "Configure webhook",
text: "Set up the webhook name and channel.",
},
{
"@type": "HowToStep",
name: "Copy webhook URL",
text: "Copy the generated webhook URL from Teams.",
},
{
"@type": "HowToStep",
name: "Configure in Postgresus",
text: "Paste the webhook URL into Postgresus notifier configuration.",
},
],
}),
}}
/>
<DocsNavbarComponent />
<div className="flex min-h-screen">
{/* Sidebar */}
<DocsSidebarComponent />
{/* Main Content */}
<main className="flex-1 px-4 py-6 sm:px-6 sm:py-8 lg:px-12">
<div className="mx-auto max-w-4xl">
<article className="prose prose-blue max-w-none">
<h1 id="teams-notifications">Microsoft Teams notifications</h1>
<p className="text-lg text-gray-700">
Configure Microsoft Teams to receive instant notifications about
your PostgreSQL backup status. Get alerts for successful
backups, failures and warnings directly in your Teams channels.
</p>
<h2 id="setup-teams-webhook">Setup Teams webhook</h2>
<h3 id="open-teams-channel">1. Open your Teams channel</h3>
<p>
Navigate to the Microsoft Teams channel where you want to
receive backup notifications. Click on the three dots (
<strong></strong>) next to the channel name.
</p>
<Image
src="/images/notifier-teams/image-01.png"
alt="Open Teams channel"
width={800}
height={500}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="access-workflows">2. Access workflows</h3>
<p>
From the channel menu, select{" "}
<strong>&quot;Workflows&quot;</strong> to open the Power
Automate integration.
</p>
<Image
src="/images/notifier-teams/image-02.png"
alt="Access Workflows"
width={500}
height={400}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="create-new-workflow">3. Create new workflow</h3>
<p>
In the Workflows panel, click on{" "}
<strong>&quot;Create&quot;</strong> or search for{" "}
<strong>
&quot;Post to a channel when a webhook request is
received&quot;
</strong>{" "}
template.
</p>
<Image
src="/images/notifier-teams/image-03.png"
alt="Create new workflow"
width={500}
height={400}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="select-webhook-template">4. Select webhook template</h3>
<p>
Choose the{" "}
<strong>
&quot;Post to a channel when a webhook request is
received&quot;
</strong>{" "}
template from the available options.
</p>
<Image
src="/images/notifier-teams/image-04.png"
alt="Select webhook template"
width={500}
height={400}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="configure-webhook">5. Configure webhook</h3>
<p>
Set up the webhook by providing a name (e.g.,{" "}
<strong>&quot;Postgresus Backup Notifications&quot;</strong>)
and confirm the channel where notifications will be posted.
</p>
<Image
src="/images/notifier-teams/image-05.png"
alt="Configure webhook"
width={500}
height={400}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="copy-webhook-url">6. Copy webhook URL</h3>
<p>
After creating the workflow, you&apos;ll see the{" "}
<strong>HTTP POST URL</strong>. Copy this URL - you&apos;ll need
it for Postgresus configuration.
</p>
<Image
src="/images/notifier-teams/image-06.png"
alt="Copy webhook URL"
width={500}
height={500}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h2 id="configure-postgresus">Configure in Postgresus</h2>
<h3 id="add-teams-notifier">1. Add Teams notifier</h3>
<p>
In Postgresus, navigate to the notifiers settings and add a new
Microsoft Teams notifier. Paste the webhook URL you copied from
Teams.
</p>
<Image
src="/images/notifier-teams/image-07.png"
alt="Configure Teams in Postgresus"
width={500}
height={400}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="test-notification">2. Test the notification</h3>
<p>
After configuring the webhook, test the notification to ensure
it&apos;s working correctly. You should receive a test message
in your selected Teams channel.
</p>
<p>
That&apos;s it! Your Microsoft Teams channel is now configured
to receive PostgreSQL backup notifications from Postgresus.
</p>
{/* Navigation */}
<div className="mt-12 border-t border-gray-200 pt-8">
<a
href="/notifiers"
className="inline-flex items-center font-semibold text-blue-600 hover:text-blue-800"
>
Back to notifiers
</a>
</div>
</article>
</div>
</main>
{/* Table of Contents */}
<DocTableOfContentComponent />
</div>
</>
);
}

1081
app/page.tsx Normal file

File diff suppressed because it is too large Load Diff

154
app/password/page.tsx Normal file
View File

@@ -0,0 +1,154 @@
import type { Metadata } from "next";
import { CopyButton } from "../components/CopyButton";
import DocsNavbarComponent from "../components/DocsNavbarComponent";
import DocsSidebarComponent from "../components/DocsSidebarComponent";
import DocTableOfContentComponent from "../components/DocTableOfContentComponent";
export const metadata: Metadata = {
title: "Reset password - Postgresus Documentation",
description:
"Learn how to reset user passwords in Postgresus using the built-in command-line tool. Quick and secure password recovery for your PostgreSQL backup system.",
keywords: [
"Postgresus password reset",
"reset user password",
"PostgreSQL backup password",
"Docker password recovery",
"password recovery",
"Postgresus authentication",
],
openGraph: {
title: "Reset Password - Postgresus Documentation",
description:
"Learn how to reset user passwords in Postgresus using the built-in command-line tool. Quick and secure password recovery for your PostgreSQL backup system.",
type: "article",
url: "https://postgresus.com/password",
},
twitter: {
card: "summary",
title: "Reset Password - Postgresus Documentation",
description:
"Learn how to reset user passwords in Postgresus using the built-in command-line tool. Quick and secure password recovery for your PostgreSQL backup system.",
},
alternates: {
canonical: "https://postgresus.com/password",
},
robots: "index, follow",
};
export default function PasswordResetPage() {
const resetPasswordCommand = `docker exec -it postgresus ./main --new-password="YourNewSecurePassword123" --email="admin"`;
return (
<>
{/* JSON-LD Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "TechArticle",
headline: "Reset Password - Postgresus Documentation",
description:
"Learn how to reset user passwords in Postgresus using the built-in command-line tool.",
author: {
"@type": "Organization",
name: "Postgresus",
},
publisher: {
"@type": "Organization",
name: "Postgresus",
logo: {
"@type": "ImageObject",
url: "https://postgresus.com/logo.svg",
},
},
datePublished: "2025-01-01",
dateModified: "2025-01-01",
}),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "HowTo",
name: "How to reset Postgresus user password",
description:
"Step-by-step guide to reset user passwords in Postgresus",
step: [
{
"@type": "HowToStep",
name: "Run password reset command",
text: "Execute the docker exec command with your new password and user email.",
itemListElement: [
{
"@type": "HowToDirection",
text: "Use docker exec to run the password reset command inside the Postgresus container",
},
],
},
{
"@type": "HowToStep",
name: "Verify password change",
text: "Log in to Postgresus with your new password to confirm the change was successful.",
},
],
}),
}}
/>
<DocsNavbarComponent />
<div className="flex min-h-screen">
{/* Sidebar */}
<DocsSidebarComponent />
{/* Main Content */}
<main className="flex-1 px-4 py-6 sm:px-6 sm:py-8 lg:px-12">
<div className="mx-auto max-w-4xl">
<article className="prose prose-blue max-w-none">
<h1 id="reset-password">Reset user password</h1>
<h2 id="reset-password-command">Reset password command</h2>
<p>
To reset a user&apos;s password, use the following command on
the server where Postgresus is running:
</p>
<div className="relative my-6">
<pre className="overflow-x-auto rounded-lg bg-gray-900 p-4 text-sm text-gray-100">
<code>{resetPasswordCommand}</code>
</pre>
<div className="absolute right-2 top-2">
<CopyButton text={resetPasswordCommand} />
</div>
</div>
<h2 id="parameters">Parameters</h2>
<p>The command accepts the following parameters:</p>
<ul>
<li>
<strong>--new-password</strong>: The new password. Make sure
it&apos;s secure and contains a mix of letters, numbers and
special characters.
</li>
<li>
<strong>--email</strong>: The email address of the user whose
password you want to reset (e.g., <code>admin</code>,{" "}
<code>user@example.com</code>).
</li>
</ul>
</article>
</div>
</main>
{/* Table of Contents */}
<DocTableOfContentComponent />
</div>
</>
);
}

11
app/robots.ts Normal file
View File

@@ -0,0 +1,11 @@
import { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: "https://postgresus.com/sitemap.xml",
};
}

63
app/sitemap.ts Normal file
View File

@@ -0,0 +1,63 @@
import { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = "https://postgresus.com";
const currentDate = new Date().toISOString();
return [
{
url: baseUrl,
lastModified: currentDate,
changeFrequency: "weekly",
priority: 1.0,
},
{
url: `${baseUrl}/installation`,
lastModified: currentDate,
changeFrequency: "weekly",
priority: 0.9,
},
{
url: `${baseUrl}/password`,
lastModified: currentDate,
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${baseUrl}/storages`,
lastModified: currentDate,
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${baseUrl}/storages/google-drive`,
lastModified: currentDate,
changeFrequency: "monthly",
priority: 0.7,
},
{
url: `${baseUrl}/storages/cloudflare-r2`,
lastModified: currentDate,
changeFrequency: "monthly",
priority: 0.7,
},
{
url: `${baseUrl}/notifiers`,
lastModified: currentDate,
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${baseUrl}/notifiers/slack`,
lastModified: currentDate,
changeFrequency: "monthly",
priority: 0.7,
},
{
url: `${baseUrl}/notifiers/teams`,
lastModified: currentDate,
changeFrequency: "monthly",
priority: 0.7,
},
];
}

View File

@@ -0,0 +1,220 @@
import type { Metadata } from "next";
import DocsNavbarComponent from "../../components/DocsNavbarComponent";
import DocsSidebarComponent from "../../components/DocsSidebarComponent";
import DocTableOfContentComponent from "../../components/DocTableOfContentComponent";
import Image from "next/image";
export const metadata: Metadata = {
title: "How to use Postgresus with Cloudflare R2 | Postgresus",
description:
"Step-by-step guide to configure Cloudflare R2 storage for PostgreSQL backups with Postgresus. Learn how to set up S3-compatible storage with R2.",
keywords: [
"Postgresus",
"Cloudflare R2",
"PostgreSQL backup",
"S3 storage",
"cloud storage",
"database backup",
],
openGraph: {
title: "How to use Postgresus with Cloudflare R2 | Postgresus",
description:
"Step-by-step guide to configure Cloudflare R2 storage for PostgreSQL backups with Postgresus. Learn how to set up S3-compatible storage with R2.",
type: "article",
url: "https://postgresus.com/storages/cloudflare-r2",
},
twitter: {
card: "summary",
title: "How to use Postgresus with Cloudflare R2 | Postgresus",
description:
"Step-by-step guide to configure Cloudflare R2 storage for PostgreSQL backups with Postgresus. Learn how to set up S3-compatible storage with R2.",
},
alternates: {
canonical: "https://postgresus.com/storages/cloudflare-r2",
},
robots: "index, follow",
};
export default function CloudflareR2Page() {
return (
<>
{/* JSON-LD Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "HowTo",
name: "How to use Postgresus with Cloudflare R2",
description:
"Step-by-step guide to configure Cloudflare R2 storage for PostgreSQL backups with Postgresus",
step: [
{
"@type": "HowToStep",
name: "Fill your bucket name",
text: "Enter your R2 bucket name in the storage configuration.",
},
{
"@type": "HowToStep",
name: "Set the region",
text: 'In region field, fill "auto"',
},
{
"@type": "HowToStep",
name: "Generate an access key ID & secret access key",
text: "In the Cloudflare dashboard, go to R2 → API → Manage API Tokens. Create the token and grant it the permissions you need.",
},
{
"@type": "HowToStep",
name: "Find your account ID",
text: "On any R2 page in the dashboard, you'll see your Account ID near the top.",
},
{
"@type": "HowToStep",
name: "Construct the S3 endpoint",
text: "Replace <ACCOUNT_ID> with the value from your dashboard in the format: https://<ACCOUNT_ID>.r2.cloudflarestorage.com",
},
],
}),
}}
/>
<DocsNavbarComponent />
<div className="flex min-h-screen">
{/* Sidebar */}
<DocsSidebarComponent />
{/* Main Content */}
<main className="flex-1 px-4 py-6 sm:px-6 sm:py-8 lg:px-12">
<div className="mx-auto max-w-4xl">
<article className="prose prose-blue max-w-none">
<h1 id="cloudflare-r2">Cloudflare R2 storage</h1>
<p className="text-lg text-gray-700">
To use Cloudflare R2 as an S3-compatible storage for your
PostgreSQL backups, you&apos;ll need to configure your R2 bucket
credentials and endpoint.
</p>
<h2 id="configuration-steps">Configuration steps</h2>
<h3 id="fill-bucket-name">1. Fill your bucket name</h3>
<p>Enter your R2 bucket name in the storage configuration:</p>
<Image
src="/images/cloudflare-r2-storage/image-1.webp"
alt="Fill your bucket name in Cloudflare R2"
width={500}
height={300}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="set-region">2. Set the region</h3>
<p>
In the region field, fill <code>&quot;auto&quot;</code>
</p>
<h3 id="generate-access-key">
3. Generate an Access Key ID &amp; Secret Access Key
</h3>
<p>
In the Cloudflare dashboard, go to{" "}
<strong>R2 API Manage API Tokens</strong>. Create a new
token and grant it the permissions you need (e.g.{" "}
<strong>&quot;Object Read &amp; Write&quot;</strong>).
</p>
<p>When the token is created, you&apos;ll see:</p>
<ul>
<li>
<strong>Access Key ID</strong> (the token&apos;s ID)
</li>
<li>
<strong>Secret Access Key</strong> (the SHA-256 hash of the
token value)
</li>
</ul>
<p>Copy both values to Postgresus:</p>
<Image
src="/images/cloudflare-r2-storage/image-2.gif"
alt="Generate Access Key ID and Secret Access Key"
width={1000}
height={600}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="find-account-id">4. Find your account ID</h3>
<p>
On any R2 page in the dashboard, you&apos;ll see your Account ID
near the top (or in your account settings):
</p>
<Image
src="/images/cloudflare-r2-storage/image-3.webp"
alt="Find your Account ID in Cloudflare dashboard"
width={600}
height={600}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="construct-endpoint">5. Construct the S3 endpoint</h3>
<p>Use the following format for your S3 endpoint:</p>
<pre>
<code>https://&lt;ACCOUNT_ID&gt;.r2.cloudflarestorage.com</code>
</pre>
<p>
Replace <code>&lt;ACCOUNT_ID&gt;</code> with the value from your
dashboard and enter it in Postgresus.
</p>
<p>
That&apos;s it! Your configuration should now look like this:
</p>
<Image
src="/images/cloudflare-r2-storage/image-4.png"
alt="Configuration complete"
width={500}
height={600}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<p>
Your Postgresus is now ready to use Cloudflare R2 as storage for
your PostgreSQL backups.
</p>
{/* Navigation */}
<div className="mt-12 border-t border-gray-200 pt-8">
<a
href="/storages"
className="inline-flex items-center font-semibold text-blue-600 hover:text-blue-800"
>
Back to storages
</a>
</div>
</article>
</div>
</main>
{/* Table of Contents */}
<DocTableOfContentComponent />
</div>
</>
);
}

View File

@@ -0,0 +1,313 @@
import type { Metadata } from "next";
import DocsNavbarComponent from "../../components/DocsNavbarComponent";
import DocsSidebarComponent from "../../components/DocsSidebarComponent";
import DocTableOfContentComponent from "../../components/DocTableOfContentComponent";
import Image from "next/image";
export const metadata: Metadata = {
title: "How to connect Google Drive to Postgresus | Postgresus",
description:
"Step-by-step guide to configure Google Drive storage for PostgreSQL backups with Postgresus. Learn how to set up Google Cloud project and OAuth.",
keywords: [
"Postgresus",
"Google Drive",
"PostgreSQL backup",
"Google Cloud",
"OAuth",
"cloud storage",
"database backup",
],
openGraph: {
title: "How to connect Google Drive to Postgresus | Postgresus",
description:
"Step-by-step guide to configure Google Drive storage for PostgreSQL backups with Postgresus. Learn how to set up Google Cloud project and OAuth.",
type: "article",
url: "https://postgresus.com/storages/google-drive",
},
twitter: {
card: "summary",
title: "How to connect Google Drive to Postgresus | Postgresus",
description:
"Step-by-step guide to configure Google Drive storage for PostgreSQL backups with Postgresus. Learn how to set up Google Cloud project and OAuth.",
},
alternates: {
canonical: "https://postgresus.com/storages/google-drive",
},
robots: "index, follow",
};
export default function GoogleDrivePage() {
return (
<>
{/* JSON-LD Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "HowTo",
name: "How to connect Google Drive to Postgresus",
description:
"Step-by-step guide to configure Google Drive storage for PostgreSQL backups with Postgresus",
step: [
{
"@type": "HowToStep",
name: "Create new project",
text: "Go to Google Cloud Console and create a new project.",
},
{
"@type": "HowToStep",
name: "Enable Google Drive API",
text: "Go to API & Services tab, then to API library and enable Google Drive API.",
},
{
"@type": "HowToStep",
name: "Configure consent screen",
text: "Go to Credentials → Create credentials → Configure consent screen and fill required data.",
},
{
"@type": "HowToStep",
name: "Create OAuth client ID",
text: "Go to Credentials → Create credentials → OAuth client ID.",
},
{
"@type": "HowToStep",
name: "Configure application settings",
text: "Set application type to Web application and configure authorized origins and redirect URIs.",
},
{
"@type": "HowToStep",
name: "Add scope",
text: 'Go to Data Access and add scope "/auth/drive.file".',
},
{
"@type": "HowToStep",
name: "Publish the app",
text: "Go to Audience and publish the app.",
},
{
"@type": "HowToStep",
name: "Sign in via Google account",
text: "Fill credentials data and sign in with your Google Account.",
},
],
}),
}}
/>
<DocsNavbarComponent />
<div className="flex min-h-screen">
{/* Sidebar */}
<DocsSidebarComponent />
{/* Main Content */}
<main className="flex-1 px-4 py-6 sm:px-6 sm:py-8 lg:px-12">
<div className="mx-auto max-w-4xl">
<article className="prose prose-blue max-w-none">
<h1 id="google-drive">Google Drive storage</h1>
<p className="text-lg text-gray-700">
To keep your backups in Google Drive, you need to create a
Google Cloud project to access the Google Drive API, then sign
in via your Google Account.
</p>
<h2 id="create-google-cloud-project">
Create Google Cloud project
</h2>
<h3 id="create-new-project">1. Create new project</h3>
<p>
Go to{" "}
<a
href="https://console.cloud.google.com/"
target="_blank"
rel="noopener noreferrer"
>
https://console.cloud.google.com/
</a>{" "}
and choose <strong>&quot;new project&quot;</strong> (top left).
</p>
<h3 id="enable-google-drive-api">2. Enable Google Drive API</h3>
<p>
Go to <strong>&quot;API &amp; Services&quot;</strong> tab, then
to <strong>&quot;API library&quot;</strong>. Choose{" "}
<strong>Google Drive API</strong> and enable it:
</p>
<Image
src="/images/google-drive-storage/image-1.webp"
alt="Enable Google Drive API"
width={500}
height={300}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="configure-consent-screen">3. Configure consent screen</h3>
<p>
Go to <strong>&quot;Credentials&quot;</strong> {" "}
<strong>&quot;Create credentials&quot;</strong> {" "}
<strong>&quot;Configure consent screen&quot;</strong> and fill
any data there:
</p>
<Image
src="/images/google-drive-storage/image-2.webp"
alt="Configure consent screen"
width={500}
height={300}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="create-oauth-client-id">4. Create OAuth client ID</h3>
<p>
Go to <strong>&quot;Credentials&quot;</strong> {" "}
<strong>&quot;Create credentials&quot;</strong> {" "}
<strong>&quot;OAuth client ID&quot;</strong>:
</p>
<Image
src="/images/google-drive-storage/image-3.webp"
alt="Create OAuth client ID"
width={500}
height={300}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="configure-application-settings">
5. Configure application settings
</h3>
<p>Fill the following data:</p>
<ul>
<li>
<strong>Application type:</strong> Web application
</li>
<li>
<strong>Authorized JavaScript origins:</strong>{" "}
<code>https://postgresus.com</code>
</li>
<li>
<strong>Authorized redirect URIs:</strong>{" "}
<code>https://postgresus.com/storages/google-oauth</code>
</li>
</ul>
<p>Then copy the credentials:</p>
<Image
src="/images/google-drive-storage/image-4.webp"
alt="Configure application settings - part 1"
width={1000}
height={600}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<Image
src="/images/google-drive-storage/image-5.png"
alt="Configure application settings - part 2"
width={450}
height={600}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="add-scope">6. Add scope</h3>
<p>
Go to <strong>&quot;Data Access&quot;</strong> and add scope{" "}
<code>&quot;/auth/drive.file&quot;</code>:
</p>
<Image
src="/images/google-drive-storage/image-6.png"
alt="Add scope"
width={600}
height={600}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="publish-app">7. Publish the app</h3>
<p>
Go to <strong>&quot;Audience&quot;</strong> and publish the app:
</p>
<Image
src="/images/google-drive-storage/image-7.png"
alt="Publish the app"
width={600}
height={600}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h2 id="sign-in-google-account">Sign in via Google account</h2>
<h3 id="fill-credentials">1. Fill credentials data</h3>
<p>Fill the credentials from the previous steps in Postgresus:</p>
<Image
src="/images/google-drive-storage/image-8.png"
alt="Fill credentials data"
width={600}
height={600}
className="my-6 rounded-lg border border-gray-200"
loading="lazy"
/>
<h3 id="choose-account">2. Choose your account</h3>
<p>Choose your Google account to sign in.</p>
<h3 id="handle-security-warning">3. Handle security warning</h3>
<p>
If you see a warning, click{" "}
<strong>&quot;Advanced&quot;</strong> (left bottom corner) and
choose <strong>&quot;Proceed anyway&quot;</strong>.
</p>
<p>
<strong>Note:</strong> This warning appears because your app is
not yet verified by Google. It&apos;s safe to proceed for your
own application.
</p>
<p>
That&apos;s it! Your Google Drive is now connected to Postgresus
and ready to store your PostgreSQL backups.
</p>
{/* Navigation */}
<div className="mt-12 border-t border-gray-200 pt-8">
<a
href="/storages"
className="inline-flex items-center font-semibold text-blue-600 hover:text-blue-800"
>
Back to storages
</a>
</div>
</article>
</div>
</main>
{/* Table of Contents */}
<DocTableOfContentComponent />
</div>
</>
);
}

View File

@@ -0,0 +1,54 @@
"use client";
import { useEffect } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
robots: "noindex, nofollow",
};
export default function GoogleOAuthPage() {
useEffect(() => {
// Get current URL parameters
const urlParams = new URLSearchParams(window.location.search);
// Extract state and code from current URL
const state = urlParams.get("state");
const code = urlParams.get("code");
if (state && code) {
try {
// Parse the StorageOauthDto from the state parameter
const oauthDto = JSON.parse(decodeURIComponent(state));
// Update the authCode field with the received code
oauthDto.authCode = code;
// Construct the redirect URL with the updated DTO
const redirectUrl = `${
oauthDto.redirectUrl
}?oauthDto=${encodeURIComponent(JSON.stringify(oauthDto))}`;
// Redirect to the constructed URL
window.location.href = redirectUrl;
} catch (error) {
console.error("Error parsing state parameter:", error);
}
} else {
console.error("Missing state or code parameter");
}
}, []);
return (
<div
style={{
textAlign: "center",
padding: "50px",
fontFamily: "Arial, sans-serif",
}}
>
<h2>Processing OAuth...</h2>
<p>Please wait while we redirect you back to the application.</p>
</div>
);
}

131
app/storages/page.tsx Normal file
View File

@@ -0,0 +1,131 @@
import type { Metadata } from "next";
import Link from "next/link";
import DocsNavbarComponent from "../components/DocsNavbarComponent";
import DocsSidebarComponent from "../components/DocsSidebarComponent";
import DocTableOfContentComponent from "../components/DocTableOfContentComponent";
export const metadata: Metadata = {
title: "Storages - Postgresus Documentation",
description:
"List of supported storage destinations for Postgresus backups including local storage, S3, Cloudflare R2, Google Drive, NAS and Dropbox.",
keywords: [
"Postgresus storages",
"backup storage",
"S3 storage",
"Google Drive backup",
"Cloudflare R2",
"NAS backup",
"Dropbox backup",
"local storage",
],
openGraph: {
title: "Storages - Postgresus Documentation",
description:
"List of supported storage destinations for Postgresus backups including local storage, S3, Cloudflare R2, Google Drive, NAS and Dropbox.",
type: "article",
url: "https://postgresus.com/storages",
},
twitter: {
card: "summary",
title: "Storages - Postgresus Documentation",
description:
"List of supported storage destinations for Postgresus backups including local storage, S3, Cloudflare R2, Google Drive, NAS and Dropbox.",
},
alternates: {
canonical: "https://postgresus.com/storages",
},
robots: "index, follow",
};
export default function StoragesPage() {
return (
<>
{/* JSON-LD Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "TechArticle",
headline: "Storages - Postgresus Documentation",
description:
"List of supported storage destinations for Postgresus backups including local storage, S3, Cloudflare R2, Google Drive, NAS and Dropbox.",
author: {
"@type": "Organization",
name: "Postgresus",
},
publisher: {
"@type": "Organization",
name: "Postgresus",
logo: {
"@type": "ImageObject",
url: "https://postgresus.com/logo.svg",
},
},
datePublished: "2025-01-01",
dateModified: "2025-01-01",
}),
}}
/>
<DocsNavbarComponent />
<div className="flex min-h-screen">
{/* Sidebar */}
<DocsSidebarComponent />
{/* Main Content */}
<main className="flex-1 px-4 py-6 sm:px-6 sm:py-8 lg:px-12">
<div className="mx-auto max-w-4xl">
<article className="prose prose-blue max-w-none">
<h1 id="storages">Storages</h1>
<p className="text-lg text-gray-700">
Postgresus supports multiple storage destinations for your
PostgreSQL backups. Choose where to store your backup files
based on your infrastructure and requirements.
</p>
<h2 id="supported-storages">Supported storages</h2>
<ul>
<li>
<strong>Local Storage</strong> - Store backups directly on
your server or VPS
</li>
<li>
<strong>S3</strong> - Amazon S3 and S3-compatible storage
services
</li>
<li>
<Link
href="/storages/cloudflare-r2"
className="font-semibold text-blue-600 hover:text-blue-800"
>
Cloudflare R2
</Link>{" "}
- S3-compatible object storage from Cloudflare
</li>
<li>
<Link
href="/storages/google-drive"
className="font-semibold text-blue-600 hover:text-blue-800"
>
Google Drive
</Link>{" "}
- Cloud storage from Google
</li>
<li>
<strong>NAS</strong> - Network-attached storage devices
</li>
</ul>
</article>
</div>
</main>
{/* Table of Contents */}
<DocTableOfContentComponent />
</div>
</>
);
}

383
blog.html
View File

@@ -1,383 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Minified TailwindCSS -->
<link rel="stylesheet" href="styles.min.css" />
<!-- Primary Meta Tags -->
<title>Blog | Postgresus</title>
<meta name="title" content="Blog | Postgresus" />
<meta
name="description"
content="Learn how to configure Postgresus with different storage providers and notification systems. Step-by-step guides for PostgreSQL backup management."
/>
<meta
name="keywords"
content="Postgresus blog, PostgreSQL backup guides, cloud storage setup, Slack notifications, Google Drive, Cloudflare R2"
/>
<link rel="canonical" href="https://postgresus.com/blog" />
<!-- Favicons -->
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="apple-touch-icon" href="favicon.svg" />
<link rel="shortcut icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Jost:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet"
/>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-GE01THYR9X"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-GE01THYR9X');
</script>
</head>
<body>
<!------------------ NAVBAR ------------------------->
<nav
class="fixed left-0 right-0 top-0 z-50 flex h-[60px] justify-center bg-white px-4 sm:h-[70px] md:h-[80px]"
>
<div
class="flex w-[320px] min-w-0 max-w-[320px] grow items-center border-b sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<a href="/" class="flex items-center">
<img
src="logo.svg"
loading="eager"
alt="Postgresus logo"
width="35"
height="35"
class="flex-shrink-0 sm:h-[40px] sm:w-[40px] md:h-[50px] md:w-[50px]"
/>
<div class="ml-2 text-lg font-bold sm:ml-3 sm:text-xl md:ml-4 md:text-2xl">
Postgresus
</div>
</a>
<div
class="ml-auto mr-4 hidden gap-3 text-base sm:mr-6 md:mr-10 lg:flex lg:gap-5 lg:text-lg"
>
<a class="hover:opacity-70" href="/#guide">Guide</a>
<a class="hover:opacity-70" href="/#installation">Installation</a>
<a class="hover:opacity-70" href="/#features">Features</a>
<a class="hover:opacity-70" href="https://t.me/postgresus_community" target="_blank"
>Community</a
>
</div>
<a
class="ml-auto lg:ml-0"
href="https://github.com/RostislavDugin/postgresus"
target="_blank"
rel="noopener noreferrer"
>
<div
class="flex items-center rounded-lg border bg-[#f5f7f9] px-2 py-1 hover:bg-gray-100 md:px-4 md:py-2"
>
<img
src="images/index/github.svg"
class="mr-1 sm:mr-2 md:mr-3"
alt="GitHub icon"
width="16"
height="16"
style="width: 16px; height: 16px"
loading="eager"
/>
<span class="text-sm sm:text-base">Star on GitHub</span>
</div>
</a>
</div>
</nav>
<!------------------ END OF NAVBAR ------------------>
<!------------------ MAIN CONTENT ------------------------->
<div class="flex justify-center py-[50px] pt-[110px] md:py-[100px] md:pt-[180px]">
<main
class="w-[320px] max-w-[320px] sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<div class="max-w-[800px]">
<h1 class="mb-6 text-2xl font-bold sm:text-4xl">Blog</h1>
<p class="mb-12 max-w-[600px] text-lg text-gray-700">
Learn how to configure Postgresus with different storage providers and notification
systems. Step-by-step guides to help you get the most out of your PostgreSQL backup
solution.
</p>
<!-- Blog Posts -->
<div class="space-y-8">
<!-- Post 1: Cloudflare R2 -->
<article
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md"
>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-orange-100">
<svg class="h-6 w-6 text-orange-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
</div>
<div class="flex-1">
<h2 class="mb-3 text-xl font-semibold">
<a href="cloudflare-r2-storage" class="text-gray-900 hover:text-blue-600">
How to use Postgresus with Cloudflare R2
</a>
</h2>
<p class="mb-4 text-gray-600">
Learn how to configure Cloudflare R2 as your S3-compatible storage for
PostgreSQL backups. Step-by-step guide to set up credentials, endpoints, and
bucket configuration.
</p>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="rounded-full bg-orange-100 px-3 py-1 text-sm text-orange-800"
>Storage</span
>
<span class="rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-800"
>Cloud</span
>
</div>
<a
href="cloudflare-r2-storage"
class="font-medium text-blue-600 hover:text-blue-800"
>
Read more →
</a>
</div>
</div>
</div>
</article>
<!-- Post 2: Google Drive -->
<article
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md"
>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-green-100">
<svg class="h-6 w-6 text-green-600" fill="currentColor" viewBox="0 0 24 24">
<path
d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"
/>
</svg>
</div>
</div>
<div class="flex-1">
<h2 class="mb-3 text-xl font-semibold">
<a href="google-drive-storage" class="text-gray-900 hover:text-blue-600">
How to connect Google Drive to Postgresus
</a>
</h2>
<p class="mb-4 text-gray-600">
Complete guide to setting up Google Drive as your backup storage. Learn how to
create a Google Cloud project, configure OAuth, and connect your Drive account.
</p>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="rounded-full bg-green-100 px-3 py-1 text-sm text-green-800"
>Storage</span
>
<span class="rounded-full bg-yellow-100 px-3 py-1 text-sm text-yellow-800"
>OAuth</span
>
</div>
<a
href="google-drive-storage"
class="font-medium text-blue-600 hover:text-blue-800"
>
Read more →
</a>
</div>
</div>
</div>
</article>
<!-- Post 3: Slack Notifications -->
<article
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md"
>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-purple-100">
<svg class="h-6 w-6 text-purple-600" fill="currentColor" viewBox="0 0 24 24">
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
/>
</svg>
</div>
</div>
<div class="flex-1">
<h2 class="mb-3 text-xl font-semibold">
<a href="notifier-slack" class="text-gray-900 hover:text-blue-600">
How to send notifications from Postgresus to Slack
</a>
</h2>
<p class="mb-4 text-gray-600">
Set up Slack notifications for your PostgreSQL backups. Learn how to create a
Slack bot, configure permissions, and get channel IDs for backup notifications.
</p>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="rounded-full bg-purple-100 px-3 py-1 text-sm text-purple-800"
>Notifications</span
>
<span class="rounded-full bg-indigo-100 px-3 py-1 text-sm text-indigo-800"
>Bot</span
>
</div>
<a href="notifier-slack" class="font-medium text-blue-600 hover:text-blue-800">
Read more →
</a>
</div>
</div>
</div>
</article>
<!-- Post 4: Teams Notifications -->
<article
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md"
>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-blue-100">
<svg class="h-6 w-6 text-blue-600" fill="currentColor" viewBox="0 0 24 24">
<path
d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"
/>
</svg>
</div>
</div>
<div class="flex-1">
<h2 class="mb-3 text-xl font-semibold">
<a href="notifier-teams" class="text-gray-900 hover:text-blue-600">
How to send notifications from Postgresus to Microsoft Teams
</a>
</h2>
<p class="mb-4 text-gray-600">
Configure Microsoft Teams notifications for your PostgreSQL backups using Power
Automate. Step-by-step guide to create an HTTP-triggered flow and connect it to
your Teams channel.
</p>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-800"
>Notifications</span
>
<span class="rounded-full bg-cyan-100 px-3 py-1 text-sm text-cyan-800"
>Power Automate</span
>
</div>
<a href="notifier-teams" class="font-medium text-blue-600 hover:text-blue-800">
Read more →
</a>
</div>
</div>
</div>
</article>
</div>
<!-- Navigation -->
<div class="mt-12 border-t border-gray-200 pt-8">
<a
href="/"
class="inline-flex items-center font-semibold text-blue-600 hover:text-blue-800"
>
← Back to main page
</a>
</div>
</div>
</main>
</div>
<!------------------ END OF MAIN CONTENT ------------------>
<!------------------ FOOTER SECTION -------------------------------->
<div id="footer" class="flex justify-center bg-blue-600 py-[50px] md:py-[50px]">
<div
class="w-[320px] max-w-[320px] sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<div class="flex flex-col items-center space-y-4">
<div class="flex flex-wrap justify-center gap-6 text-white">
<a
href="https://github.com/RostislavDugin/postgresus"
target="_blank"
class="transition-colors hover:text-blue-200"
>
GitHub
</a>
<a
href="https://t.me/postgresus_community"
target="_blank"
class="transition-colors hover:text-blue-200"
>
Community
</a>
<a
href="https://rostislav-dugin.com"
target="_blank"
class="transition-colors hover:text-blue-200"
>Developer
</a>
</div>
<p class="text-center text-sm text-white">&copy; 2025 Postgresus. All rights reserved.</p>
</div>
</div>
</div>
<!------------------ END OF FOOTER SECTION ------------------------->
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function (m, e, t, r, i, k, a) {
m[i] =
m[i] ||
function () {
(m[i].a = m[i].a || []).push(arguments);
};
m[i].l = 1 * new Date();
for (var j = 0; j < document.scripts.length; j++) {
if (document.scripts[j].src === r) {
return;
}
}
((k = e.createElement(t)),
(a = e.getElementsByTagName(t)[0]),
(k.async = 1),
(k.src = r),
a.parentNode.insertBefore(k, a));
})(window, document, 'script', 'https://mc.yandex.ru/metrika/tag.js?id=103482608', 'ym');
ym(103482608, 'init', {
ssr: true,
clickmap: true,
ecommerce: 'dataLayer',
accurateTrackBounce: true,
trackLinks: true,
});
</script>
<noscript
><div>
<img
src="https://mc.yandex.ru/watch/103482608"
style="position: absolute; left: -9999px"
alt=""
/></div
></noscript>
<!-- /Yandex.Metrika counter -->
</body>
</html>

View File

@@ -1,319 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Minified TailwindCSS -->
<link rel="stylesheet" href="styles.min.css" />
<!-- Primary Meta Tags -->
<title>How to use Postgresus with Cloudflare R2 | Postgresus</title>
<meta name="title" content="How to use Postgresus with Cloudflare R2 | Postgresus" />
<meta
name="description"
content="Step-by-step guide to configure Cloudflare R2 storage for PostgreSQL backups with Postgresus. Learn how to set up S3-compatible storage with R2."
/>
<meta
name="keywords"
content="Postgresus, Cloudflare R2, PostgreSQL backup, S3 storage, cloud storage, database backup"
/>
<link rel="canonical" href="https://postgresus.com/cloudflare-r2-storage" />
<!-- Favicons -->
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="apple-touch-icon" href="favicon.svg" />
<link rel="shortcut icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Jost:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="index.css" />
<script async src="https://www.googletagmanager.com/gtag/js?id=G-GE01THYR9X"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-GE01THYR9X');
</script>
</head>
<body>
<!------------------ NAVBAR ------------------------->
<nav
class="fixed left-0 right-0 top-0 z-50 flex h-[60px] justify-center bg-white px-4 sm:h-[70px] md:h-[80px]"
>
<div
class="flex w-[320px] min-w-0 max-w-[320px] grow items-center border-b sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<a href="/" class="flex items-center">
<img
src="logo.svg"
loading="eager"
alt="Postgresus logo"
width="35"
height="35"
class="flex-shrink-0 sm:h-[40px] sm:w-[40px] md:h-[50px] md:w-[50px]"
/>
<div class="ml-2 text-lg font-bold sm:ml-3 sm:text-xl md:ml-4 md:text-2xl">
Postgresus
</div>
</a>
</div>
</nav>
<!------------------ END OF NAVBAR ------------------>
<!------------------ MAIN CONTENT ------------------------->
<div class="flex justify-center py-[50px] pt-[110px] md:py-[100px] md:pt-[180px]">
<main
class="w-[320px] max-w-[320px] sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<div class="max-w-[800px]">
<h1 class="mb-6 text-2xl font-bold sm:text-4xl">
How to use Postgresus with Cloudflare R2?
</h1>
<p class="mb-8 max-w-[500px] text-lg text-gray-700">
To obtain the credentials and endpoint for using your R2 bucket as an S3-compatible
source, you'll need to:
</p>
<div class="space-y-8">
<!-- Step 1 -->
<div class="border-l-4 border-blue-500 pl-6">
<h2 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-sm text-white"
>1</span
>
Fill your bucket name
</h2>
<p class="mb-2 text-gray-700">
Enter your R2 bucket name in the storage configuration.
</p>
</div>
<img
src="images/cloudflare-r2-storage/image-1.webp"
alt="Step 1"
class="mb-8 max-w-[500px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
<!-- Step 2 -->
<div class="border-l-4 border-blue-500 pl-6">
<h2 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-sm text-white"
>2</span
>
Set the region
</h2>
<p class="mb-2 text-gray-700">
In region field, fill
<code class="rounded bg-gray-100 px-2 py-1 text-sm">"auto"</code>
</p>
</div>
<!-- Step 3 -->
<div class="border-l-4 border-blue-500 pl-6">
<h2 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-sm text-white"
>3</span
>
Generate an Access Key ID & Secret Access Key
</h2>
<div class="space-y-4">
<p class="text-gray-700">
In the Cloudflare dashboard, go to <strong>R2 → API → Manage API Tokens</strong>.
Create the token.
</p>
<p class="text-gray-700">
Grant it the permissions you need (e.g. <strong>"Object Read & Write"</strong>).
</p>
<div class="rounded-lg bg-blue-50 p-4">
<p class="mb-2 text-gray-700">When the token is created you'll see:</p>
<ul class="list-inside list-disc space-y-1 text-gray-700">
<li><strong>Access Key ID</strong> (the token's ID)</li>
<li>
<strong>Secret Access Key</strong> (the SHA-256 hash of the token value)
</li>
</ul>
<p class="mt-2 font-semibold text-blue-600">Copy both now to Postgresus.</p>
</div>
</div>
</div>
<img
src="images/cloudflare-r2-storage/image-2.gif"
alt="Step 2"
class="mb-8 max-w-[1000px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
<!-- Step 4 -->
<div class="border-l-4 border-blue-500 pl-6">
<h2 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-sm text-white"
>4</span
>
Find your Account ID
</h2>
<p class="text-gray-700">
On any R2 page in the dashboard, you'll see your Account ID near the top (or in your
account settings).
</p>
</div>
<img
src="images/cloudflare-r2-storage/image-3.webp"
alt="Step 3"
class="mb-8 max-w-[1000px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
<!-- Step 5 -->
<div class="border-l-4 border-blue-500 pl-6">
<h2 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-sm text-white"
>5</span
>
Construct the S3 endpoint
</h2>
<div class="space-y-4">
<div class="rounded-lg bg-gray-100 p-4">
<code class="text-sm">https://&lt;ACCOUNT_ID&gt;.r2.cloudflarestorage.com</code>
</div>
<p class="text-gray-700">
Replace
<code class="rounded bg-gray-100 px-2 py-1 text-sm">&lt;ACCOUNT_ID&gt;</code> with
the value from your dashboard.
</p>
<p class="text-gray-700">Fill it to Postgresus as well.</p>
</div>
</div>
<!-- Final Step -->
<div class="max-w-[500px] rounded-lg border border-green-200 bg-green-50 p-6">
<h2 class="mb-3 text-xl font-semibold text-green-800">🎉 Configuration Complete!</h2>
<p class="text-green-700">
Now you have all fields filled and your Postgresus is ready to use Cloudflare R2 as
storage for your PostgreSQL backups.
</p>
</div>
<img
src="images/cloudflare-r2-storage/image-4.png"
alt="Step 4"
class="mb-8 max-w-[1000px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<!-- Navigation -->
<div class="mt-12 border-t border-gray-200 pt-8">
<a
href="/"
class="inline-flex items-center font-semibold text-blue-600 hover:text-blue-800"
>
← Back to main page
</a>
</div>
</div>
</main>
</div>
<!------------------ END OF MAIN CONTENT ------------------>
<!------------------ FOOTER SECTION -------------------------------->
<div id="footer" class="flex justify-center bg-blue-600 py-[50px] md:py-[50px]">
<div
class="w-[320px] max-w-[320px] sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<div class="flex flex-col items-center space-y-4">
<div class="flex flex-wrap justify-center gap-6 text-white">
<a
href="https://github.com/RostislavDugin/postgresus"
target="_blank"
class="transition-colors hover:text-blue-200"
>
GitHub
</a>
<a
href="https://t.me/postgresus_community"
target="_blank"
class="transition-colors hover:text-blue-200"
>
Community
</a>
<a
href="https://rostislav-dugin.com"
target="_blank"
class="transition-colors hover:text-blue-200"
>
Developer
</a>
</div>
<p class="text-center text-sm text-white">&copy; 2025 Postgresus. All rights reserved.</p>
</div>
</div>
</div>
<!------------------ END OF FOOTER SECTION ------------------------->
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function (m, e, t, r, i, k, a) {
m[i] =
m[i] ||
function () {
(m[i].a = m[i].a || []).push(arguments);
};
m[i].l = 1 * new Date();
for (var j = 0; j < document.scripts.length; j++) {
if (document.scripts[j].src === r) {
return;
}
}
((k = e.createElement(t)),
(a = e.getElementsByTagName(t)[0]),
(k.async = 1),
(k.src = r),
a.parentNode.insertBefore(k, a));
})(window, document, 'script', 'https://mc.yandex.ru/metrika/tag.js?id=103482608', 'ym');
ym(103482608, 'init', {
ssr: true,
clickmap: true,
ecommerce: 'dataLayer',
accurateTrackBounce: true,
trackLinks: true,
});
</script>
<noscript
><div>
<img
src="https://mc.yandex.ru/watch/103482608"
style="position: absolute; left: -9999px"
alt=""
/></div
></noscript>
<!-- /Yandex.Metrika counter -->
</body>
</html>

23
eslint.config.mjs Normal file
View File

@@ -0,0 +1,23 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
{
rules: {
"@next/next/no-img-element": "off",
},
},
]);
export default eslintConfig;

View File

@@ -1,461 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Minified TailwindCSS -->
<link rel="stylesheet" href="styles.min.css" />
<!-- Primary Meta Tags -->
<title>How to connect Google Drive to Postgresus | Postgresus</title>
<meta name="title" content="How to connect Google Drive to Postgresus | Postgresus" />
<meta
name="description"
content="Step-by-step guide to configure Google Drive storage for PostgreSQL backups with Postgresus. Learn how to set up Google Cloud project and OAuth."
/>
<meta
name="keywords"
content="Postgresus, Google Drive, PostgreSQL backup, Google Cloud, OAuth, cloud storage, database backup"
/>
<link rel="canonical" href="https://postgresus.com/google-drive-storage" />
<!-- Favicons -->
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="apple-touch-icon" href="favicon.svg" />
<link rel="shortcut icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Jost:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="index.css" />
<script async src="https://www.googletagmanager.com/gtag/js?id=G-GE01THYR9X"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-GE01THYR9X');
</script>
</head>
<body>
<!------------------ NAVBAR ------------------------->
<nav
class="fixed left-0 right-0 top-0 z-50 flex h-[60px] justify-center bg-white px-4 sm:h-[70px] md:h-[80px]"
>
<div
class="flex w-[320px] min-w-0 max-w-[320px] grow items-center border-b sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<a href="/" class="flex items-center">
<img
src="logo.svg"
loading="eager"
alt="Postgresus logo"
width="35"
height="35"
class="flex-shrink-0 sm:h-[40px] sm:w-[40px] md:h-[50px] md:w-[50px]"
/>
<div class="ml-2 text-lg font-bold sm:ml-3 sm:text-xl md:ml-4 md:text-2xl">
Postgresus
</div>
</a>
</div>
</nav>
<!------------------ END OF NAVBAR ------------------>
<!------------------ MAIN CONTENT ------------------------->
<div class="flex justify-center py-[50px] pt-[110px] md:py-[100px] md:pt-[180px]">
<main
class="w-[320px] max-w-[320px] sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<div class="max-w-[800px]">
<h1 class="mb-6 text-2xl font-bold sm:text-4xl">
How to connect Google Drive to Postgresus?
</h1>
<p class="mb-8 max-w-[500px] text-lg text-gray-700">
To keep your backups in Google Drive you need:
</p>
<div class="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="rounded-lg bg-blue-50 p-4">
<h3 class="mb-2 font-semibold text-blue-800">Step 1</h3>
<p class="text-blue-700">
Create Google Cloud project so you can access Google Drive API
</p>
</div>
<div class="rounded-lg bg-blue-50 p-4">
<h3 class="mb-2 font-semibold text-blue-800">Step 2</h3>
<p class="text-blue-700">Sign in via your Google Account</p>
</div>
</div>
<div class="space-y-8">
<!-- How to create Google Cloud project -->
<div class="pl-6">
<h2 class="mb-4 text-2xl font-semibold">How to create Google Cloud project?</h2>
<div class="space-y-6">
<!-- Step 1 -->
<div class="border-l-4 border-blue-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-sm text-white"
>1</span
>
Create new project
</h3>
<div class="space-y-2">
<p class="text-gray-700">
Go to
<a
href="https://console.cloud.google.com/"
target="_blank"
class="text-blue-600 underline hover:text-blue-800"
>https://console.cloud.google.com/</a
>
</p>
<p class="text-gray-700">Choose <strong>"new project"</strong> (top left)</p>
</div>
</div>
<!-- Step 2 -->
<div class="border-l-4 border-blue-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-sm text-white"
>2</span
>
Enable Google Drive API
</h3>
<div class="space-y-2">
<p class="text-gray-700">
Go to <strong>"API & Services"</strong> tab, then to
<strong>"API library"</strong>
</p>
<p class="pb-10 text-gray-700">
Choose <strong>Google Drive API</strong> and enable it:
</p>
</div>
<img
src="images/google-drive-storage/image-1.webp"
alt="Step 2"
class="mb-8 max-w-[500px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<!-- Step 3 -->
<div class="border-l-4 border-blue-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-sm text-white"
>3</span
>
Configure consent screen
</h3>
<p class="pb-10 text-gray-700">
Go to <strong>"Credentials"</strong><strong>"Create credentials"</strong>
<strong>"Configure consent screen"</strong> and fill any data there:
</p>
<img
src="images/google-drive-storage/image-2.webp"
alt="Step 3"
class="mb-8 max-w-[500px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<!-- Step 4 -->
<div class="border-l-4 border-blue-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-sm text-white"
>4</span
>
Create OAuth client ID
</h3>
<p class="pb-10 text-gray-700">
Go to <strong>"Credentials"</strong><strong>"Create credentials"</strong>
<strong>"OAuth client ID"</strong>:
</p>
<img
src="images/google-drive-storage/image-3.webp"
alt="Step 4"
class="mb-8 max-w-[500px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<!-- Step 5 -->
<div class="border-l-4 border-blue-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-sm text-white"
>5</span
>
Configure application settings
</h3>
<p class="mb-4 text-gray-700">Fill the following data:</p>
<div class="space-y-2">
<div class="rounded-lg bg-gray-100 p-3">
<p class="text-sm"><strong>Application type:</strong> Web application</p>
</div>
<div class="rounded-lg bg-gray-100 p-3">
<p class="text-sm">
<strong>Authorized JavaScript origins:</strong>
<code class="rounded bg-gray-200 px-2 py-1 text-sm"
>https://postgresus.com</code
>
</p>
</div>
<div class="rounded-lg bg-gray-100 p-3">
<p class="text-sm">
<strong>Authorized redirect URIs:</strong>
<code class="rounded bg-gray-200 px-2 py-1 text-sm"
>https://postgresus.com/storages/google-oauth</code
>
</p>
</div>
<div class="mt-3 text-gray-700">And copy credentials</div>
</div>
<div class="pb-10"></div>
<img
src="images/google-drive-storage/image-4.webp"
alt="Step 5 - 1"
class="mb-8 max-w-[1000px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
<img
src="images/google-drive-storage/image-5.png"
alt="Step 5 - 2"
class="mb-8 max-w-[1000px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<!-- Step 6 -->
<div class="border-l-4 border-blue-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-sm text-white"
>6</span
>
Add scope
</h3>
<p class="pb-5 text-gray-700">
Go to <strong>"Data Access"</strong> and add scope
<code class="rounded bg-gray-100 px-2 py-1 text-sm">"/auth/drive.file"</code>
</p>
<img
src="images/google-drive-storage/image-6.png"
alt="Step 6"
class="mb-8 max-w-[1000px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<!-- Step 7 -->
<div class="border-l-4 border-blue-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-sm text-white"
>7</span
>
Publish the app
</h3>
<p class="pb-10 text-gray-700">
Go to <strong>"Audience"</strong> and publish an app
</p>
<img
src="images/google-drive-storage/image-7.png"
alt="Step 7"
class="mb-8 max-w-[1000px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
</div>
</div>
<!-- Sign in via Google Account -->
<div class="pl-6">
<h2 class="mb-4 text-2xl font-semibold">Sign in via your Google Account</h2>
<div class="space-y-6">
<!-- Step 1 -->
<div class="border-l-4 border-green-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-green-500 text-sm text-white"
>1</span
>
Fill credentials data
</h3>
<p class="pb-10 text-gray-700">Fill data from the previous step:</p>
<img
src="images/google-drive-storage/image-8.png"
alt="Step 1"
class="mb-8 max-w-[1000px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<!-- Step 2 -->
<div class="border-l-4 border-green-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-green-500 text-sm text-white"
>2</span
>
Choose your account
</h3>
<p class="text-gray-700">Choose your account to sign in</p>
</div>
<!-- Step 3 -->
<div class="border-l-4 border-green-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-green-500 text-sm text-white"
>3</span
>
Handle security warning
</h3>
<div class="space-y-2">
<p class="text-gray-700">
If you see a warning, click <strong>"Advanced"</strong> (left bottom corner)
and choose <strong>"Proceed anyway"</strong>
</p>
<div class="rounded-lg bg-yellow-50 p-4">
<p class="text-sm text-yellow-800">
⚠️ This warning appears because your app is not yet verified by Google. It's
safe to proceed for your own application.
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Final Step -->
<div class="max-w-[500px] rounded-lg border border-green-200 bg-green-50 p-6">
<h2 class="mb-3 text-xl font-semibold text-green-800">🎉 Setup Complete!</h2>
<p class="text-green-700">
Your Google Drive is now connected to Postgresus and ready to store your PostgreSQL
backups securely in the cloud.
</p>
</div>
</div>
<!-- Navigation -->
<div class="mt-12 border-t border-gray-200 pt-8">
<a
href="/"
class="inline-flex items-center font-semibold text-blue-600 hover:text-blue-800"
>
← Back to main page
</a>
</div>
</div>
</main>
</div>
<!------------------ END OF MAIN CONTENT ------------------>
<!------------------ FOOTER SECTION -------------------------------->
<div id="footer" class="flex justify-center bg-blue-600 py-[50px] md:py-[50px]">
<div
class="w-[320px] max-w-[320px] sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<div class="flex flex-col items-center space-y-4">
<div class="flex flex-wrap justify-center gap-6 text-white">
<a
href="https://github.com/RostislavDugin/postgresus"
target="_blank"
class="transition-colors hover:text-blue-200"
>
GitHub
</a>
<a
href="https://t.me/postgresus_community"
target="_blank"
class="transition-colors hover:text-blue-200"
>
Community
</a>
<a
href="https://rostislav-dugin.com"
target="_blank"
class="transition-colors hover:text-blue-200"
>
Developer
</a>
</div>
<p class="text-center text-sm text-white">&copy; 2025 Postgresus. All rights reserved.</p>
</div>
</div>
</div>
<!------------------ END OF FOOTER SECTION ------------------------->
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function (m, e, t, r, i, k, a) {
m[i] =
m[i] ||
function () {
(m[i].a = m[i].a || []).push(arguments);
};
m[i].l = 1 * new Date();
for (var j = 0; j < document.scripts.length; j++) {
if (document.scripts[j].src === r) {
return;
}
}
((k = e.createElement(t)),
(a = e.getElementsByTagName(t)[0]),
(k.async = 1),
(k.src = r),
a.parentNode.insertBefore(k, a));
})(window, document, 'script', 'https://mc.yandex.ru/metrika/tag.js?id=103482608', 'ym');
ym(103482608, 'init', {
ssr: true,
clickmap: true,
ecommerce: 'dataLayer',
accurateTrackBounce: true,
trackLinks: true,
});
</script>
<noscript
><div>
<img
src="https://mc.yandex.ru/watch/103482608"
style="position: absolute; left: -9999px"
alt=""
/></div
></noscript>
<!-- /Yandex.Metrika counter -->
</body>
</html>

View File

@@ -1,35 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Jost', sans-serif;
line-height: 1.6;
}
html {
scroll-behavior: smooth;
}
a {
text-decoration: none;
color: inherit;
}
ul,
ol {
list-style: none;
}
button {
font-family: inherit;
cursor: pointer;
}
input,
textarea,
select {
font-family: inherit;
}

1131
index.html

File diff suppressed because it is too large Load Diff

38
next.config.ts Normal file
View File

@@ -0,0 +1,38 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
async redirects() {
return [
// Old storage URLs to new structure
{
source: "/cloudflare-r2-storage",
destination: "/storages/cloudflare-r2",
permanent: true,
},
{
source: "/google-drive-storage",
destination: "/storages/google-drive",
permanent: true,
},
// Old notifier URLs to new structure
{
source: "/notifier-slack",
destination: "/notifiers/slack",
permanent: true,
},
{
source: "/notifier-teams",
destination: "/notifiers/teams",
permanent: true,
},
// Blog removed permanently
{
source: "/blog",
destination: "/gone",
permanent: true,
},
];
},
};
export default nextConfig;

View File

@@ -1,404 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Minified TailwindCSS -->
<link rel="stylesheet" href="styles.min.css" />
<!-- Primary Meta Tags -->
<title>How to send notifications from Postgresus to Slack | Postgresus</title>
<meta name="title" content="How to send notifications from Postgresus to Slack | Postgresus" />
<meta
name="description"
content="Step-by-step guide to configure Slack notifications for PostgreSQL backups with Postgresus. Learn how to create Slack bot and get chat ID."
/>
<meta
name="keywords"
content="Postgresus, Slack notifications, PostgreSQL backup, Slack bot, chat notifications, database backup"
/>
<link rel="canonical" href="https://postgresus.com/notifier-slack" />
<!-- Favicons -->
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="apple-touch-icon" href="favicon.svg" />
<link rel="shortcut icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Jost:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="index.css" />
<script async src="https://www.googletagmanager.com/gtag/js?id=G-GE01THYR9X"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-GE01THYR9X');
</script>
</head>
<body>
<!------------------ NAVBAR ------------------------->
<nav
class="fixed left-0 right-0 top-0 z-50 flex h-[60px] justify-center bg-white px-4 sm:h-[70px] md:h-[80px]"
>
<div
class="flex w-[320px] min-w-0 max-w-[320px] grow items-center border-b sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<a href="/" class="flex items-center">
<img
src="logo.svg"
loading="eager"
alt="Postgresus logo"
width="35"
height="35"
class="flex-shrink-0 sm:h-[40px] sm:w-[40px] md:h-[50px] md:w-[50px]"
/>
<div class="ml-2 text-lg font-bold sm:ml-3 sm:text-xl md:ml-4 md:text-2xl">
Postgresus
</div>
</a>
</div>
</nav>
<!------------------ END OF NAVBAR ------------------>
<!------------------ MAIN CONTENT ------------------------->
<div class="flex justify-center py-[50px] pt-[110px] md:py-[100px] md:pt-[180px]">
<main
class="w-[320px] max-w-[320px] sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<div class="max-w-[800px]">
<h1 class="mb-6 max-w-[500px] text-2xl font-bold sm:text-4xl">
How to send notifications from Postgresus to Slack?
</h1>
<p class="mb-8 max-w-[500px] text-lg text-gray-700">
To send notifications to Slack you need to follow 2 steps:
</p>
<div class="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="rounded-lg bg-blue-50 p-4">
<h3 class="mb-2 font-semibold text-blue-800">Step 1</h3>
<p class="text-blue-700">Create Slack bot</p>
</div>
<div class="rounded-lg bg-blue-50 p-4">
<h3 class="mb-2 font-semibold text-blue-800">Step 2</h3>
<p class="text-blue-700">Get chat ID (channel, group or user)</p>
</div>
</div>
<div class="space-y-8">
<!-- How to create Slack bot -->
<div class="pl-6">
<h2 class="mb-4 text-2xl font-semibold">How to create Slack bot?</h2>
<div class="space-y-4">
<div class="border-l-4 border-green-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-green-500 text-sm text-white"
>1</span
>
Create the app
</h3>
<div class="space-y-2">
<p class="text-gray-700">
Go to
<a
href="https://api.slack.com/apps"
target="_blank"
class="text-blue-600 underline hover:text-blue-800"
>https://api.slack.com/apps</a
>
</p>
<p class="text-gray-700">
Choose <strong>"create an app"</strong>
<strong>"create an app from scratch"</strong>
</p>
</div>
</div>
<div class="rounded-lg bg-orange-50 p-4">
<div class="mb-2 font-semibold text-orange-800">After bot creation:</div>
</div>
<div class="border-l-4 border-green-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-green-500 text-sm text-white"
>2</span
>
Configure OAuth & Permissions
</h3>
<p class="mb-2 text-gray-700">Go to <strong>"OAuth & Permissions"</strong> tab</p>
<img
src="images/notifier-slack/image-1.png"
alt="Step 1"
class="mb-8 mt-5 max-w-[1000px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<div class="border-l-4 border-green-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-green-500 text-sm text-white"
>3</span
>
Add bot scopes
</h3>
<p class="mb-2 text-gray-700">Add to bot scopes:</p>
<ul class="ml-4 list-inside list-disc space-y-1 text-gray-700">
<li><code class="rounded bg-gray-100 px-2 py-1 text-sm">im:write</code></li>
<li><code class="rounded bg-gray-100 px-2 py-1 text-sm">chat:write</code></li>
<li>
<code class="rounded bg-gray-100 px-2 py-1 text-sm">channels:history</code>
</li>
<li>
<code class="rounded bg-gray-100 px-2 py-1 text-sm">channels:join</code>
</li>
</ul>
<img
src="images/notifier-slack/image-2.png"
alt="Step 1"
class="mb-8 mt-5 max-w-[500px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<div class="border-l-4 border-green-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-green-500 text-sm text-white"
>4</span
>
Install to workspace
</h3>
<p class="text-gray-700">
Click <strong>"Install to {YOUR_WORKSPACE}"</strong> and complete steps
</p>
<img
src="images/notifier-slack/image-3.png"
alt="Step 1"
class="mb-8 mt-5 max-w-[500px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
</div>
</div>
<!-- How to get chat ID -->
<div class="pl-6">
<h2 class="mb-4 text-2xl font-semibold">How to get chat ID?</h2>
<p class="mb-4 text-gray-700">You can send messages to:</p>
<ul class="mb-6 ml-4 list-inside list-disc space-y-1 text-gray-700">
<li>Public channel</li>
<li>Private channel</li>
<li>Group</li>
<li>Direct messages</li>
</ul>
<div class="mb-6 rounded-lg border border-yellow-200 bg-yellow-50 p-4">
<div class="mb-2 font-semibold text-yellow-800">⚠️ Warning</div>
<ul class="list-inside list-disc space-y-1 text-yellow-700">
<li>
For sending messages to <strong>private channel</strong> bot should be added via
<code class="rounded bg-yellow-100 px-2 py-1 text-sm">@invite</code> command
</li>
<li>
For sending messages to <strong>group</strong>, group should be created with bot
initially
</li>
</ul>
</div>
<div class="space-y-6">
<div class="border-l-4 border-indigo-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-indigo-500 text-sm text-white"
>1</span
>
Get channel or group ID
</h3>
<div class="space-y-2">
<p class="text-gray-700">Navigate to chat and take ID from URL</p>
<div class="rounded-lg bg-gray-100 p-3">
<p class="text-sm text-gray-600">
💡 <strong>Tip:</strong> ID usually starts from
<code class="rounded bg-gray-200 px-2 py-1 text-sm">"C"</code> letter
</p>
</div>
<img
src="images/notifier-slack/image-4.png"
alt="Step 1"
class="mb-8 mt-5 max-w-[500px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
</div>
<div class="border-l-4 border-indigo-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-indigo-500 text-sm text-white"
>2</span
>
Get user ID
</h3>
<div class="space-y-2">
<p class="text-gray-700">
Go to <strong>"Profile"</strong> and click
<strong>"copy link to profile"</strong>
</p>
<img
src="images/notifier-slack/image-5.png"
alt="Step 1"
class="mb-8 mt-5 max-w-[500px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
<p class="text-gray-700">Then extract end part of URL, this is your user ID</p>
<img
src="images/notifier-slack/image-6.png"
alt="Step 1"
class="mb-8 mt-5 max-w-[500px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
</div>
</div>
</div>
<!-- Final Step -->
<div class="max-w-[500px] rounded-lg border border-green-200 bg-green-50 p-6">
<h2 class="mb-3 text-xl font-semibold text-green-800">🎉 Setup Complete!</h2>
<p class="text-green-700">
Copy both token and ID to Postgresus, notification should appear and you're ready to
receive backup notifications in Slack!
</p>
</div>
<img
src="images/notifier-slack/image-7.png"
alt="Step 1"
class="mb-8 mt-5 max-w-[1000px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<!-- Navigation -->
<div class="mt-12 border-t border-gray-200 pt-8">
<a
href="/"
class="inline-flex items-center font-semibold text-blue-600 hover:text-blue-800"
>
← Back to main page
</a>
</div>
</div>
</main>
</div>
<!------------------ END OF MAIN CONTENT ------------------>
<!------------------ FOOTER SECTION -------------------------------->
<div id="footer" class="flex justify-center bg-blue-600 py-[50px] md:py-[50px]">
<div
class="w-[320px] max-w-[320px] sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<div class="flex flex-col items-center space-y-4">
<div class="flex flex-wrap justify-center gap-6 text-white">
<a
href="https://github.com/RostislavDugin/postgresus"
target="_blank"
class="transition-colors hover:text-blue-200"
>
GitHub
</a>
<a
href="https://t.me/postgresus_community"
target="_blank"
class="transition-colors hover:text-blue-200"
>
Community
</a>
<a
href="https://rostislav-dugin.com"
target="_blank"
class="transition-colors hover:text-blue-200"
>
Developer
</a>
</div>
<p class="text-center text-sm text-white">&copy; 2025 Postgresus. All rights reserved.</p>
</div>
</div>
</div>
<!------------------ END OF FOOTER SECTION ------------------------->
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function (m, e, t, r, i, k, a) {
m[i] =
m[i] ||
function () {
(m[i].a = m[i].a || []).push(arguments);
};
m[i].l = 1 * new Date();
for (var j = 0; j < document.scripts.length; j++) {
if (document.scripts[j].src === r) {
return;
}
}
((k = e.createElement(t)),
(a = e.getElementsByTagName(t)[0]),
(k.async = 1),
(k.src = r),
a.parentNode.insertBefore(k, a));
})(window, document, 'script', 'https://mc.yandex.ru/metrika/tag.js?id=103482608', 'ym');
ym(103482608, 'init', {
ssr: true,
clickmap: true,
ecommerce: 'dataLayer',
accurateTrackBounce: true,
trackLinks: true,
});
</script>
<noscript
><div>
<img
src="https://mc.yandex.ru/watch/103482608"
style="position: absolute; left: -9999px"
alt=""
/></div
></noscript>
<!-- /Yandex.Metrika counter -->
</body>
</html>

View File

@@ -1,364 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Minified TailwindCSS -->
<link rel="stylesheet" href="styles.min.css" />
<!-- Primary Meta Tags -->
<title>How to send notifications from Postgresus to Teams | Postgresus</title>
<meta name="title" content="How to send notifications from Postgresus to Teams | Postgresus" />
<meta
name="description"
content="Step-by-step guide to configure Microsoft Teams notifications for PostgreSQL backups with Postgresus using Power Automate. Create an HTTP-triggered flow in Teams and copy its URL."
/>
<meta
name="keywords"
content="Postgresus, Microsoft Teams notifications, Power Automate, HTTP request trigger, PostgreSQL backup, Teams channel notifications, database backup"
/>
<link rel="canonical" href="https://postgresus.com/notifier-teams" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://postgresus.com/notifier-teams" />
<meta property="og:title" content="How to send notifications from Postgresus to Teams | Postgresus" />
<meta
property="og:description"
content="Step-by-step guide to configure Microsoft Teams notifications for PostgreSQL backups with Postgresus using Power Automate. Create an HTTP-triggered flow in Teams and copy its URL."
/>
<meta property="og:image" content="https://postgresus.com/logo.svg" />
<!-- Twitter -->
<meta property="twitter:card" content="summary" />
<meta property="twitter:url" content="https://postgresus.com/notifier-teams" />
<meta property="twitter:title" content="How to send notifications from Postgresus to Teams | Postgresus" />
<meta
property="twitter:description"
content="Step-by-step guide to configure Microsoft Teams notifications for PostgreSQL backups with Postgresus using Power Automate. Create an HTTP-triggered flow in Teams and copy its URL."
/>
<meta property="twitter:image" content="https://postgresus.com/logo.svg" />
<!-- Favicons -->
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="apple-touch-icon" href="favicon.svg" />
<link rel="shortcut icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Jost:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="index.css" />
<script async src="https://www.googletagmanager.com/gtag/js?id=G-GE01THYR9X"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-GE01THYR9X');
</script>
</head>
<body>
<!------------------ NAVBAR ------------------------->
<nav
class="fixed left-0 right-0 top-0 z-50 flex h-[60px] justify-center bg-white px-4 sm:h-[70px] md:h-[80px]"
>
<div
class="flex w-[320px] min-w-0 max-w-[320px] grow items-center border-b sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<a href="/" class="flex items-center">
<img
src="logo.svg"
loading="eager"
alt="Postgresus logo"
width="35"
height="35"
class="flex-shrink-0 sm:h-[40px] sm:w-[40px] md:h-[50px] md:w-[50px]"
/>
<div class="ml-2 text-lg font-bold sm:ml-3 sm:text-xl md:ml-4 md:text-2xl">
Postgresus
</div>
</a>
</div>
</nav>
<!------------------ END OF NAVBAR ------------------>
<!------------------ MAIN CONTENT ------------------------->
<div class="flex justify-center py-[50px] pt-[110px] md:py-[100px] md:pt-[180px]">
<main
class="w-[320px] max-w-[320px] sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<div class="max-w-[800px]">
<h1 class="mb-6 max-w-[650px] text-2xl font-bold sm:text-4xl">
How to send notifications from Postgresus to Microsoft Teams
</h1>
<p class="mb-8 max-w-[600px] text-lg text-gray-700">
To send notifications to Microsoft Teams you will:
</p>
<div class="space-y-8">
<!-- How to create Power Automate URL -->
<div class="pl-6">
<h2 class="mb-4 text-2xl font-semibold">How to create a Power Automate URL (inside Teams)</h2>
<div class="space-y-4">
<!-- Step 1 -->
<div class="border-l-4 border-green-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-green-500 text-sm text-white"
>1</span
>
Open Teams and go to the Teams section on the left side
</h3>
<div class="space-y-2">
<p class="text-gray-700">
If you don't have a group yet, create it as shown in the screenshot, if it already exists, then go to section #2
</p>
<img
src="images/notifier-teams/image-01.png"
alt="Open Power Automate app in Microsoft Teams"
class="mb-4 mt-3 max-w-[600px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
</div>
<!-- Step 2 -->
<div class="border-l-4 border-green-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-green-500 text-sm text-white"
>2</span
>
Creation of flow
</h3>
<p class="mb-2 text-gray-700">
Select the desired channel and click on the three dots. Then select Workflows
</p>
<img
src="images/notifier-teams/image-02.png"
alt="Select environment in Power Automate"
class="mb-4 mt-3 max-w-[600px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<!-- Step 3 -->
<div class="border-l-4 border-green-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-green-500 text-sm text-white"
>3</span
>
Create the flow (HTTP trigger)
</h3>
<p class="text-gray-700">
In the new window, select the flow type - Send webhook alerts to a channel.
</p>
<img
src="images/notifier-teams/image-03.png"
alt="Create Automated cloud flow with HTTP trigger"
class="mb-4 mt-3 max-w-[600px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<!-- Step 4 -->
<div class="border-l-4 border-green-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-green-500 text-sm text-white"
>4</span
>
(Optional) If necessary, you can change the name of the Flow (optional)
</h3>
<p class="mb-2 text-gray-700">
The name Flow does not affect its operation.
</p>
<img
src="images/notifier-teams/image-04.png"
alt="Create Automated cloud flow with HTTP trigger"
class="mb-4 mt-3 max-w-[600px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<!-- Step 5 -->
<div class="border-l-4 border-green-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-green-500 text-sm text-white"
>5</span
>
(Optional)You can choose which channel to send Flow to.
</h3>
<img
src="images/notifier-teams/image-05.png"
alt="Add Teams action: Post a message in a chat or channel"
class="mb-4 mt-3 max-w-[600px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<!-- Step 6 -->
<div class="border-l-4 border-green-500 pl-6">
<h3 class="mb-3 flex items-center text-xl font-semibold">
<span
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-green-500 text-sm text-white"
>6</span
>
Save and copy the HTTP URL
</h3>
<p class="text-gray-700">
At the bottom of the window you will have a url that you need to copy and paste into the Postgresus settings.
</p>
<img
src="images/notifier-teams/image-06.png"
alt="Copy HTTP POST URL from the trigger"
class="mb-4 mt-3 max-w-[600px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<!-- Connect in Postgresus -->
<div class="pl-6">
<h2 class="mb-4 text-2xl font-semibold">Connect the URL in Postgresus</h2>
<ol class="ml-2 list-inside list-decimal space-y-2 text-gray-700">
<li>Open <strong>Postgresus</strong>.</li>
<li>Go to <strong>Settings</strong><strong>Notifiers</strong><strong>Teams</strong>.</li>
<li>Paste the <strong>URL</strong> you copied from Teams.</li>
<li>Click <strong>Send test notification</strong> in Postgresus and check the chosen Teams channel for the message.</li>
</ol>
<img
src="images/notifier-teams/image-07.png"
alt="Paste the flow URL into Postgresus"
class="mb-8 mt-5 max-w-[600px] rounded-lg border border-gray-200 shadow-lg"
loading="lazy"
/>
</div>
<!-- Final Step -->
<div class="max-w-[600px] rounded-lg border border-green-200 bg-green-50 p-6">
<h2 class="mb-3 text-xl font-semibold text-green-800">🎉 Setup Complete!</h2>
<p class="text-green-700">
Your Power Automate flow is connected. Postgresus can now send backup notifications
directly to your selected Microsoft Teams chat or channel.
</p>
</div>
</div>
<!-- Navigation -->
<div class="mt-12 border-t border-gray-200 pt-8">
<a
href="/"
class="inline-flex items-center font-semibold text-blue-600 hover:text-blue-800"
>
← Back to main page
</a>
</div>
</div>
</main>
</div>
<!------------------ END OF MAIN CONTENT ------------------>
<!------------------ FOOTER SECTION -------------------------------->
<div id="footer" class="flex justify-center bg-blue-600 py-[50px] md:py-[50px]">
<div
class="w-[320px] max-w-[320px] sm:w-[640px] sm:max-w-[640px] lg:w-[1200px] lg:max-w-[1200px]"
>
<div class="flex flex-col items-center space-y-4">
<div class="flex flex-wrap justify-center gap-6 text-white">
<a
href="https://github.com/RostislavDugin/postgresus"
target="_blank"
class="transition-colors hover:text-blue-200"
>
GitHub
</a>
<a
href="https://t.me/postgresus_community"
target="_blank"
class="transition-colors hover:text-blue-200"
>
Community
</a>
<a
href="https://rostislav-dugin.com"
target="_blank"
class="transition-colors hover:text-blue-200"
>
Developer
</a>
</div>
<p class="text-center text-sm text-white">&copy; 2025 Postgresus. All rights reserved.</p>
</div>
</div>
</div>
<!------------------ END OF FOOTER SECTION ------------------------->
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function (m, e, t, r, i, k, a) {
m[i] =
m[i] ||
function () {
(m[i].a = m[i].a || []).push(arguments);
};
m[i].l = 1 * new Date();
for (var j = 0; j < document.scripts.length; j++) {
if (document.scripts[j].src === r) {
return;
}
}
((k = e.createElement(t)),
(a = e.getElementsByTagName(t)[0]),
(k.async = 1),
(k.src = r),
a.parentNode.insertBefore(k, a));
})(window, document, 'script', 'https://mc.yandex.ru/metrika/tag.js?id=103482608', 'ym');
ym(103482608, 'init', {
ssr: true,
clickmap: true,
ecommerce: 'dataLayer',
accurateTrackBounce: true,
trackLinks: true,
});
</script>
<noscript
><div>
<img
src="https://mc.yandex.ru/watch/103482608"
style="position: absolute; left: -9999px"
alt=""
/></div
></noscript>
<!-- /Yandex.Metrika counter -->
</body>
</html>

6531
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,26 @@
{
"name": "postgresus-website",
"version": "1.0.0",
"description": "PostgreSQL backup and monitoring tool",
"main": "index.js",
"version": "0.1.0",
"private": true,
"scripts": {
"format": "prettier --write .",
"format:check": "prettier --check .",
"build-css": "tailwindcss -i ./src/input.css -o ./styles.min.css --minify",
"watch-css": "tailwindcss -i ./src/input.css -o ./styles.min.css --watch",
"build": "npm run build-css"
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"react": "19.2.0",
"react-dom": "19.2.0",
"next": "16.0.1"
},
"author": "Postgresus",
"license": "MIT",
"devDependencies": {
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.16"
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "16.0.1"
}
}
}

7
postcss.config.mjs Normal file
View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 905 KiB

After

Width:  |  Height:  |  Size: 905 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 282 B

View File

Before

Width:  |  Height:  |  Size: 795 KiB

After

Width:  |  Height:  |  Size: 795 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

183
public/llms.txt Normal file
View File

@@ -0,0 +1,183 @@
# Postgresus - PostgreSQL Backup Tool
> Free and open source tool for PostgreSQL scheduled backups. Save them locally and to clouds. Notifications to Slack, Discord, etc.
## Project Information
- Name: Postgresus
- Type: Self-hosted PostgreSQL backup tool
- License: Apache 2.0
- Repository: https://github.com/RostislavDugin/postgresus
- Website: https://postgresus.com
- Community: https://t.me/postgresus_community
## Key Features
- Scheduled PostgreSQL backups (hourly, daily, weekly, monthly)
- Multiple storage destinations: Local, S3, Cloudflare R2, Google Drive, Dropbox, NAS and more
- Real-time notifications: Slack, Telegram, Discord, Microsoft Teams, Email, Webhooks and more
- Database health monitoring with configurable health checks
- Self-hosted via Docker with zero-config installation
- Support for PostgreSQL versions 13, 14, 15, 16, 17 and 18
- Backup compression (4-8x size reduction)
- Open source and free
## Documentation
### Installation
URL: /installation
Content: Three installation methods - automated script, Docker run, Docker Compose
### Password Reset
URL: /password
Content: How to reset user passwords using command-line tool
### Storage Configuration
URL: /storages
Content: Overview of supported storage destinations
#### Google Drive Storage
URL: /storages/google-drive
Content: Step-by-step guide to configure Google Drive storage with OAuth setup
#### Cloudflare R2 Storage
URL: /storages/cloudflare-r2
Content: Guide to configure Cloudflare R2 S3-compatible storage
### Notification Configuration
URL: /notifiers
Content: Overview of supported notification channels
#### Slack Notifications
URL: /notifiers/slack
Content: How to set up Slack webhook notifications
#### Microsoft Teams Notifications
URL: /notifiers/teams
Content: How to configure Teams webhook notifications
## Technical Details
### System Requirements
- CPU: At least 1 core
- RAM: Minimum 500 MB
- Storage: 5 GB + backup storage space
- Docker Engine 20.10+
- Docker Compose v2.0+
### Installation Options
1. **Automated Script (Recommended for Linux)**
- Installs Docker and Docker Compose automatically
- Sets up Postgresus with auto-restart
- Installation directory: /opt/postgresus
2. **Docker Run**
- Quick single-command deployment
- Data stored in ./postgresus-data
- Port: 4005
3. **Docker Compose**
- Managed deployment with configuration file
- Automatic restart on system reboot
### Backup Process
1. Select schedule (hourly, daily, weekly, monthly, specific time)
2. Configure database connection (host, port, credentials, version, SSL)
3. Choose storage destination(s)
4. Set up notification channels (optional)
5. Automated backup execution with compression
### Storage Destinations
- **Local Storage**: Direct server/VPS storage
- **S3**: Amazon S3 and S3-compatible services
- **Cloudflare R2**: S3-compatible object storage
- **Google Drive**: Cloud storage via Google Cloud API
- **NAS**: Network-attached storage devices
- **Dropbox**: Cloud storage integration
- and more
### Notification Channels
- **Slack**: Via webhook integration
- **Discord**: Channel notifications
- **Telegram**: Bot-based notifications
- **Microsoft Teams**: Workflow webhook integration
- **Email**: SMTP-based email alerts
- **Webhook**: Custom HTTP webhook integration
- and more
## Target Audience
Postgresus is suitable for:
- **Single Developers**: Managing personal projects and side projects
- **DevOps Teams**: Maintaining production databases and infrastructure
- **Organizations**: Requiring compliance, audit trails and enterprise backup solutions
- **Startups**: Need reliable backups without enterprise costs
- **System Administrators**: Managing multiple database environments
- **IT Departments**: Seeking self-hosted backup solutions
## Use Cases
- Single developers managing personal projects
- DevOps teams maintaining production databases
- Organizations requiring compliance and audit trails
- Self-hosted database backup solutions
- Multi-environment database management
- Automated disaster recovery preparation
## Backup Features
- **Compression**: Balanced compression with 4-8x size reduction
- **Scheduling**: Flexible scheduling with exact time specification
- **Health Checks**: Configurable database monitoring
- **Multi-target**: Store backups to multiple destinations simultaneously
- **Restoration**: One-click restore functionality
- **Security**: All data stays within your infrastructure
## FAQ
### What is Postgresus?
Apache 2.0 licensed, self-hosted service for backing up PostgreSQL v13-v18. Features frontend for scheduling, compression, multiple storage targets and team notifications. Designed to be more UX-friendly alternative to PgBackRest, Barman, pg_dump scripts and other traditional backup tools.
### How is Postgresus different from PgBackRest, Barman or pg_dump?
Postgresus provides a modern, user-friendly web interface instead of complex configuration files and command-line tools. While PgBackRest and Barman require extensive configuration and command-line expertise, Postgresus offers intuitive point-and-click setup. Unlike raw pg_dump scripts, it includes built-in scheduling, compression, multiple storage destinations, health monitoring and real-time notifications - all managed through a simple web UI. It's the UX-friendly alternative for developers and DevOps teams who want powerful backup capabilities without the complexity.
### Installation Time
Typically less than 2 minutes on a standard VPS using the automated installer.
### Backup Schedules
Hourly, daily, weekly or monthly cycles with exact run time specification (e.g., 04:00). Weekly schedules support specific weekdays, monthly schedules support specific calendar days.
### Storage Space
Archives saved to local volumes, S3-compatible buckets, Google Drive, Dropbox and other cloud targets. Balanced compression typically shrinks dump size by 4-8x with ~20% runtime overhead.
### Failure Notifications
Real-time notifications via email, Slack, Telegram, webhooks, Mattermost, Discord and more. DevOps teams receive immediate alerts for successes and failures.
### Security
All data executes within containers you control on servers you own. Credentials and backup files remain on your server or in your cloud account. Open source allows full code inspection.
## Developer Information
- Docker Image: rostislavdugin/postgresus:latest
- Default Port: 4005
- Data Directory: ./postgresus-data or /postgresus-data
- Configuration: Web-based UI
- API: Internal backend API
## Related Technologies
- PostgreSQL: Supported versions 13-18
- Docker: Containerization platform
- pg_dump: PostgreSQL backup utility
- S3 API: Storage protocol compatibility
- OAuth 2.0: Google Drive authentication
- Webhook: Notification integration protocol
## Keywords
PostgreSQL, backup, monitoring, database, scheduled backups, Docker, self-hosted, open source, S3, Google Drive, Slack notifications, Discord, DevOps, database monitoring, pg_dump, database restore, Cloudflare R2, Microsoft Teams, health checks, compression, automation

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,4 +0,0 @@
User-agent: *
Disallow:
Sitemap: https://postgresus.com/sitemap.xml

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://postgresus.com/</loc>
<lastmod>2025-07-16T01:03:35+00:00</lastmod>
</url>
<url>
<loc>https://postgresus.com/blog</loc>
<lastmod>2025-07-25T19:48:00+00:00</lastmod>
</url>
<url>
<loc>https://postgresus.com/cloudflare-r2-storage</loc>
<lastmod>2025-07-16T01:03:35+00:00</lastmod>
</url>
<url>
<loc>https://postgresus.com/notifier-slack</loc>
<lastmod>2025-07-16T01:03:35+00:00</lastmod>
</url>
<url>
<loc>https://postgresus.com/google-drive-storage</loc>
<lastmod>2025-07-16T01:03:35+00:00</lastmod>
</url>
</urlset>

View File

@@ -1,40 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom styles from index.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Jost', sans-serif;
line-height: 1.6;
}
html {
scroll-behavior: smooth;
}
a {
text-decoration: none;
color: inherit;
}
ul,
ol {
list-style: none;
}
button {
font-family: inherit;
cursor: pointer;
}
input,
textarea,
select {
font-family: inherit;
}

View File

@@ -1,46 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex, nofollow" />
<title>Google OAuth - Redirecting...</title>
</head>
<body>
<div style="text-align: center; padding: 50px; font-family: Arial, sans-serif">
<h2>Processing OAuth...</h2>
<p>Please wait while we redirect you back to the application.</p>
</div>
<script>
window.onload = function () {
// Get current URL parameters
const urlParams = new URLSearchParams(window.location.search);
// Extract state and code from current URL
const state = urlParams.get('state');
const code = urlParams.get('code');
if (state && code) {
try {
// Parse the StorageOauthDto from the state parameter
const oauthDto = JSON.parse(decodeURIComponent(state));
// Update the authCode field with the received code
oauthDto.authCode = code;
// Construct the redirect URL with the updated DTO
const redirectUrl = `${oauthDto.redirectUrl}?oauthDto=${encodeURIComponent(JSON.stringify(oauthDto))}`;
// Redirect to the constructed URL
window.location.href = redirectUrl;
} catch (error) {
console.error('Error parsing state parameter:', error);
}
} else {
console.error('Missing state or code parameter');
}
};
</script>
</body>
</html>

1934
styles.min.css vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./index.html', './*.html', './storages/**/*.html', './pages/**/*.html'],
theme: {
extend: {
fontFamily: {
jost: ['Jost', 'sans-serif'],
},
},
},
plugins: [],
};

34
tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}