Merge branch 'eldadfux-patch-network' into update-for-network

This commit is contained in:
Christy Jacob
2025-02-05 09:56:36 +05:30
committed by GitHub
1412 changed files with 10574 additions and 1577 deletions

View File

@@ -2,6 +2,7 @@ PUBLIC_APPWRITE_COL_MESSAGES_ID=
PUBLIC_APPWRITE_COL_THREADS_ID=
PUBLIC_APPWRITE_DB_MAIN_ID=
PUBLIC_APPWRITE_FN_TLDR_ID=
PUBLIC_APPWRITE_DASHBOARD=
PUBLIC_APPWRITE_ENDPOINT=
PUBLIC_APPWRITE_PROJECT_ID=
PUBLIC_APPWRITE_PROJECT_INIT_ID=

View File

@@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@@ -1,30 +0,0 @@
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};

View File

@@ -72,7 +72,7 @@ jobs:
echo "_APP_VERSION=${{ env.TAG }}" >> .env
echo "_APP_DOMAIN=${{ secrets.PRD_APP_DOMAIN }}" >> .env
echo "_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=${{ secrets.APP_SYSTEM_SECURITY_EMAIL_ADDRESS }}" >> .env
echo "SEMATEXT_TOKEN=${{ secrets.SEMATEXT_TOKEN }}" >> .env
echo "_APP_BETTER_STACK_INCIDENT_URL=${{ secrets.BETTER_STACK_INCIDENT_URL }}" >> .env
echo ${{ secrets.GH_REGISTRY_TOKEN }} | docker login ghcr.io --username ${{ env.REGISTRY_USERNAME }} --password-stdin
docker-compose -f ${{ env.STACK_FILE }} config

View File

@@ -74,7 +74,7 @@ jobs:
echo "_APP_VERSION=${{ env.TAG }}" >> .env
echo "_APP_DOMAIN=${{ secrets.STG_APP_DOMAIN }}" >> .env
echo "_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=${{ secrets.APP_SYSTEM_SECURITY_EMAIL_ADDRESS }}" >> .env
echo "SEMATEXT_TOKEN=${{ secrets.SEMATEXT_TOKEN }}" >> .env
echo "_APP_BETTER_STACK_INCIDENT_URL=${{ secrets.BETTER_STACK_INCIDENT_URL }}" >> .env
echo ${{ secrets.GH_REGISTRY_TOKEN }} | docker login ghcr.io --username ${{ env.REGISTRY_USERNAME }} --password-stdin
docker-compose -f ${{ env.STACK_FILE }} config

3
.gitignore vendored
View File

@@ -18,4 +18,5 @@ package-lock.json
.history
terraform/**/.t*
terraform/**/.env
terraform/**/**/*.tfstate*
terraform/**/**/*.tfstate*
/.cache

View File

@@ -1,9 +0,0 @@
{
"useTabs": false,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@@ -296,7 +296,7 @@ Configure FCM for push notification to Android and Apple devices.
#### Accordions
Use accordions to reduce page size and collapse information that's not important when a reader is skilling the page.
Use accordions to reduce page size and collapse information that's not important when a reader is scrolling the page.
```
{% accordion %}

View File

@@ -15,7 +15,7 @@ The Appwrite Website has been built with the following frameworks:
## Development
_If this is your first time setting up the repository, please run `pnpm install` inside the repo's directory._
_If this is your first time setting up the repository, please run `pnpm install` inside the repo's directory and create a `.env` file based on `.env.example`._
To get the repo up and running in your local environment, use the following command:

View File

@@ -119,23 +119,23 @@ services:
- TIME_BETWEEN_RUNS=3600
- UNUSED_TIME=6h
sematext-agent:
image: sematext/agent:latest
environment:
REGION: EU
INFRA_TOKEN: $SEMATEXT_TOKEN
deploy:
mode: global
restart_policy:
condition: any
resource-monitor:
image: ghcr.io/appwrite/monitoring:0.1.0
entrypoint: monitoring
command:
- '--url=${_APP_BETTER_STACK_INCIDENT_URL}'
- '--interval=60'
- '--cpu-limit=85'
- '--memory-limit=80'
- '--disk-limit=85'
hostname: '{{.Node.Hostname}}'
<<: *x-logging
volumes:
- /:/hostfs:ro
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
- /sys:/host/sys:ro
- /dev:/hostfs/dev:ro
- /var/run:/var/run
- /sys/kernel/debug:/sys/kernel/debug
- /mnt:/mnt:ro
deploy:
<<: *x-update-config
endpoint_mode: dnsrr
mode: global
networks:
cloud:

View File

@@ -114,23 +114,23 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
sematext-agent:
image: sematext/agent:latest
environment:
REGION: EU
INFRA_TOKEN: $SEMATEXT_TOKEN
deploy:
mode: global
restart_policy:
condition: any
resource-monitor:
image: ghcr.io/appwrite/monitoring:0.1.0
entrypoint: monitoring
command:
- '--url=${_APP_BETTER_STACK_INCIDENT_URL}'
- '--interval=60'
- '--cpu-limit=85'
- '--memory-limit=80'
- '--disk-limit=85'
hostname: '{{.Node.Hostname}}'
<<: *x-logging
volumes:
- /:/hostfs:ro
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
- /sys:/host/sys:ro
- /dev:/hostfs/dev:ro
- /var/run:/var/run
- /sys/kernel/debug:/sys/kernel/debug
- /mnt:/mnt:ro
deploy:
<<: *x-update-config
endpoint_mode: dnsrr
mode: global
networks:
cloud:

33
eslint.config.js Normal file
View File

@@ -0,0 +1,33 @@
import prettier from 'eslint-config-prettier';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import ts from 'typescript-eslint';
export default ts.config(
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}
);

View File

@@ -7,6 +7,10 @@
"type": "esm",
"property": "default",
"watch": true
},
"partials": {
"auth-security.md": "./src/partials/auth-security.md",
"prohibited-activities.md": "./src/partials/prohibited-activities.md"
}
}
]

View File

@@ -19,47 +19,48 @@
"test": "npm run test:integration && npm run test:unit",
"test:integration": "playwright test",
"test:unit": "vitest",
"optimize": "node ./scripts/optimize-assets.js"
"optimize": "node ./scripts/optimize-assets.js",
"optimize:all": "node ./scripts/optimize-all.js"
},
"packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a",
"dependencies": {
"@sentry/sveltekit": "^8.12.0",
"h3": "^1.12.0",
"sharp": "^0.33.4"
"@sentry/sveltekit": "^8.38.0",
"h3": "^1.13.0",
"sharp": "^0.33.5"
},
"devDependencies": {
"@appwrite.io/console": "^0.6.2",
"@appwrite.io/console": "^0.6.4",
"@appwrite.io/pink": "~0.26.0",
"@appwrite.io/pink-icons": "~0.26.0",
"@appwrite.io/repo": "github:appwrite/appwrite#feat-multi-region-docs",
"@internationalized/date": "3.5.0",
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.74.4",
"@playwright/test": "^1.44.1",
"@melt-ui/svelte": "^0.86.0",
"@playwright/test": "^1.49.0",
"@sveltejs/adapter-node": "^4.0.1",
"@sveltejs/enhanced-img": "^0.1.9",
"@sveltejs/kit": "^2.5.17",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@sveltejs/kit": "^2.8.1",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@tailwindcss/postcss": "4.0.0-alpha.17",
"@types/compression": "^1.7.5",
"@types/glob": "^8.1.0",
"@types/markdown-it": "^13.0.8",
"@types/markdown-it": "^13.0.9",
"@types/morgan": "^1.9.9",
"@typescript-eslint/eslint-plugin": "^7.13.1",
"@typescript-eslint/parser": "^7.13.1",
"analytics": "^0.8.14",
"clsx": "^2.1.1",
"cva": "npm:class-variance-authority@^0.7.0",
"date-fns": "^3.6.0",
"dequal": "^2.0.3",
"embla-carousel": "^8.1.5",
"embla-carousel-svelte": "^8.1.5",
"embla-carousel": "^8.4.0",
"embla-carousel-auto-scroll": "^8.5.1",
"embla-carousel-svelte": "^8.4.0",
"embla-carousel-wheel-gestures": "^8.0.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-svelte": "^2.40.0",
"eslint": "^9.15.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.0",
"fuse.js": "^7.0.0",
"highlight.js": "^11.9.0",
"globals": "^15.12.0",
"highlight.js": "^11.10.0",
"markdown-it": "^14.1.0",
"meilisearch": "^0.37.0",
"motion": "^10.18.0",
@@ -67,24 +68,26 @@
"openapi-types": "^12.1.3",
"oslllo-svg-fixer": "^3.0.0",
"plausible-tracker": "^0.3.9",
"postcss": "^8.4.39",
"postcss": "^8.4.49",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.6.6",
"remeda": "^2.10.0",
"sass": "^1.77.6",
"svelte": "^4.2.18",
"svelte-check": "^3.8.1",
"svelte-markdoc-preprocess": "^2.0.0",
"prettier-plugin-svelte": "^3.2.8",
"prettier-plugin-tailwindcss": "^0.6.8",
"remeda": "^2.17.3",
"sass": "^1.81.0",
"svelte": "^4.2.19",
"svelte-check": "^3.8.6",
"svelte-markdoc-preprocess": "^2.1.0",
"svelte-markdown": "^0.4.1",
"svgtofont": "^4.2.1",
"tailwind-merge": "^2.5.2",
"svgtofont": "^4.2.3",
"tailwind-merge": "^2.5.4",
"tailwindcss": "4.0.0-alpha.17",
"tslib": "^2.6.3",
"typescript": "^5.5.2",
"vite": "^5.3.1",
"vite-plugin-dynamic-import": "^1.5.0",
"tslib": "^2.8.1",
"typescript": "^5.6.3",
"typescript-eslint": "^8.15.0",
"vite": "^5.4.11",
"vite-plugin-dynamic-import": "^1.6.0",
"vite-plugin-image-optimizer": "^1.1.8",
"vite-plugin-manifest-sri": "^0.2.0",
"vitest": "^1.6.0"
}
}

1241
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

15
prettier.config.js Normal file
View File

@@ -0,0 +1,15 @@
/** @type {import("prettier").Config} */
export default {
useTabs: false,
tabWidth: 4,
singleQuote: true,
trailingComma: 'none',
printWidth: 100,
plugins: ['prettier-plugin-svelte', 'prettier-plugin-tailwindcss'],
overrides: [
{
files: '*.svelte',
options: { parser: 'svelte' }
}
]
};

153
scripts/optimize-all.js Normal file
View File

@@ -0,0 +1,153 @@
import { readdirSync, statSync } from 'fs';
import { join, relative, resolve } from 'path';
import sharp from 'sharp';
import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const project_root = resolve(__dirname, '..');
// Directories to search in
const search_dirs = ['src', 'static', 'routes', 'lib'];
// Directories to skip
const excluded_dirs = ['node_modules', '.svelte-kit', 'build', '.git', 'assets/'];
/**
* @type {{
* jpeg: sharp.JpegOptions,
* webp: sharp.WebpOptions,
* png: sharp.PngOptions,
* gif: sharp.GifOptions,
* avif: sharp.AvifOptions
* }}
*/
const config = {
jpeg: {
quality: 100
},
webp: {
lossless: true
},
png: {
quality: 100
},
gif: {
quality: 100
},
avif: {
lossless: true
}
};
/** @type {sharp.ResizeOptions} */
const resize_config = {
width: 1280,
height: 1280,
fit: sharp.fit.inside,
withoutEnlargement: true
};
function* walk_directory(dir) {
try {
const files = readdirSync(dir);
for (const file of files) {
const pathToFile = join(dir, file);
const relativePath = relative(project_root, pathToFile);
// Skip excluded directories
if (excluded_dirs.some((excluded) => relativePath.includes(excluded))) {
continue;
}
const isDirectory = statSync(pathToFile).isDirectory();
if (isDirectory) {
yield* walk_directory(pathToFile);
} else {
yield pathToFile;
}
}
} catch (error) {
console.error(`Error accessing directory ${dir}:`, error.message);
}
}
function is_image(file) {
const extension = file.split('.').pop()?.toLowerCase();
return extension && Object.keys(config).includes(extension);
}
function format_size(bytes) {
const sizes = ['B', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`;
}
async function optimize_image(file) {
const is_animated = file.endsWith('.gif');
const image = sharp(file, { animated: is_animated });
try {
const size_before = (await image.toBuffer()).length;
const meta = await image.metadata();
if (!meta.format || !config[meta.format]) {
console.warn(`Unsupported format for file: ${file}`);
return;
}
const buffer = await image[meta.format](config[meta.format])
.resize(resize_config)
.toBuffer();
const size_after = buffer.length;
if (size_after >= size_before) {
console.log(`Skipping ${relative(project_root, file)} - no size reduction possible`);
return;
}
const savings = (((size_before - size_after) / size_before) * 100).toFixed(2);
console.log(`Optimizing ${relative(project_root, file)}`);
console.log(` Before: ${format_size(size_before)}`);
console.log(` After: ${format_size(size_after)}`);
console.log(` Saved: ${savings}%`);
await sharp(buffer).toFile(file);
} catch (error) {
console.error(`Error processing ${file}:`, error.message);
}
}
async function main() {
let total_files = 0;
let processed_files = 0;
console.log('Starting image optimization...\n');
for (const search_dir of search_dirs) {
const full_path = join(project_root, search_dir);
try {
if (!statSync(full_path).isDirectory()) continue;
for (const file of walk_directory(full_path)) {
if (!is_image(file)) continue;
total_files++;
await optimize_image(file);
processed_files++;
}
} catch (error) {
if (error.code === 'ENOENT') {
console.log(`Directory ${search_dir} not found - skipping`);
} else {
console.error(`Error processing ${search_dir}:`, error.message);
}
}
}
console.log(`\nOptimization complete!`);
console.log(`Processed ${processed_files} of ${total_files} image files`);
}
await main();

View File

@@ -8,6 +8,7 @@
--color-primary: hsl(var(--color-primary));
--color-secondary: hsl(var(--color-secondary));
--color-accent: var(--color-secondary);
--color-smooth: var(--color-smooth);
/* pink */
--color-pink-200: hsl(var(--color-pink-hue) 98% 84%);
@@ -45,6 +46,9 @@
--color-blue-500: calc(hsl(var(--color-blue-hue) - 1) 99% 70%);
--color-blue-700: calc(hsl(var(--color-blue-hue) - 1) 42% 42%);
/* green */
--color-green-700: #0a714f;
/* secondary */
--color-secondary-100: hsl(var(--color-secondary-hue) 99% 66%);
--color-accent-200: hsl(var(--color-secondary-hue), 78%, 60%, 0.32);
@@ -53,7 +57,7 @@
--color-white: hsl(0 0% 100%);
--color-black: hsl(0 0% 0%);
--color-transparent: rgba(0, 0, 0, 0);
--color-smooth: hsl(var(--color-greyscale-hue) 6%, 10%, 0.04);
--color-offset: hsl(var(--color-greyscale-hue) 2%, 11%, 0.94);
--color-greyscale-25: hsl(var(--color-greyscale-hue) 11% 98%);
--color-greyscale-50: hsl(var(--color-greyscale-hue) 11% 94%);
--color-greyscale-100: hsl(var(--color-greyscale-hue) 6% 90%);
@@ -77,8 +81,12 @@
/* Animations */
--animate-scale-in: scale-in 200ms ease-out forwards;
--animate-text: fade-in 0.75s ease-in-out both, blur 0.75s ease-in-out both,
--animate-caret-blink: caret-blink 1s ease-in-out infinite;
--animate-text: fade 0.75s ease-in-out both, blur 0.75s ease-in-out both,
up 0.75s ease-in-out both;
--animate-scroll: scroll 60s linear infinite;
--animate-fade-in: fade 0.5s ease-in-out both;
--animate-marquee: marquee var(--speed, 30s) linear infinite var(--direction, forwards);
/* Pink polyfills */
--transition: 0.2s;
@@ -93,6 +101,18 @@
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes blur {
0% {
filter: blur(5px);
@@ -120,6 +140,21 @@
}
}
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-100%);
}
}
@keyframes marquee {
to {
transform: translateX(-50%);
}
}
/* Fonts */
--font-family-sans: 'Inter', arial, sans-serif;
--font-family-mono: 'Fira Code', monospace;
@@ -128,9 +163,12 @@
--font-family-archia: 'Archia', arial, sans-serif;
/* Font sizes */
--font-size-x-micro: 0.625rem;
--font-size-x-micro--line-height: 0.875rem;
--font-size-x-micro--letter-spacing: var(--letter-spacing-tighter);
--font-size-micro: 0.75rem;
--font-size-micro--line-height: 1rem;
--font-size-micro--letter-spacing: var(--letter-spacing-loose);
--font-size-micro--letter-spacing: var(--letter-spacing-tighter);
--font-size-caption: 0.875rem;
--font-size-caption--line-height: 1.375rem;
--font-size-caption--letter-spacing: var(--letter-spacing-tight);
@@ -190,6 +228,7 @@
--color-accent: var(--color-pink-600);
--color-badge-bg: var(--color-badge-bg-light);
--color-badge-border: var(--color-badge-border-light);
--color-smooth: hsl(var(--color-greyscale-hue) 6%, 10%, 0.04);
}
/* dark theme */
@@ -198,6 +237,7 @@
--color-secondary: var(--color-greyscale-300);
--color-badge-bg: var(--color-badge-bg-dark);
--color-badge-border: var(--color-badge-border-dark);
--color-smooth: hsl(0 0%, 100%, 0.06);
}
/* Container */

View File

@@ -28,6 +28,51 @@ const redirecter: Handle = async ({ event, resolve }) => {
return await resolve(event);
};
const securityheaders: Handle = async ({ event, resolve }) => {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
(event.locals as { nonce: string }).nonce = nonce;
const response = await resolve(event, {
transformPageChunk: ({ html }) => {
return html.replace(/%sveltekit.nonce%/g, nonce);
}
});
const cspDirectives = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'self' https://www.youtube.com https://*.vimeo.com",
'block-all-mixed-content',
'upgrade-insecure-requests',
"connect-src 'self' https://cloud.appwrite.io",
"frame-src 'self' https://www.youtube.com https://status.appwrite.online https://www.youtube-nocookie.com https://player.vimeo.com"
];
// Set security headers
response.headers.set('Content-Security-Policy', cspDirectives.join('; '));
// HTTP Strict Transport Security
// max-age is set to 1 year in seconds
response.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
// X-Content-Type-Options
response.headers.set('X-Content-Type-Options', 'nosniff');
// X-Frame-Options
response.headers.set('X-Frame-Options', 'DENY');
return response;
};
const bannerRewriter: Handle = async ({ event, resolve }) => {
const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('%aw_banner_key%', BANNER_KEY)
@@ -35,5 +80,5 @@ const bannerRewriter: Handle = async ({ event, resolve }) => {
return response;
};
export const handle = sequence(Sentry.sentryHandle(), redirecter, bannerRewriter);
export const handle = sequence(Sentry.sentryHandle(), redirecter, bannerRewriter, securityheaders);
export const handleError = Sentry.handleErrorWithSentry();

View File

@@ -69,17 +69,4 @@ export const trackEvent = async (name: string, data: object = {}) => {
}
};
export function isTrackingAllowed() {
if (ENV.TEST) {
return;
}
if (window.navigator?.doNotTrack) {
if (navigator.doNotTrack === '1' || navigator.doNotTrack === 'yes') {
return false;
} else {
return true;
}
} else {
return true;
}
}
export const isTrackingAllowed = () => !ENV.TEST;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -158,7 +158,6 @@
import { postController } from './post';
import Post from './post/post.svelte';
import { anyify } from '$lib/utils/anyify';
import Badge from '$lib/components/ui/Badge.svelte';
/* Basic Animation setup */
let scrollInfo = {
@@ -508,10 +507,6 @@
display: flex;
align-items: center;
gap: 0.75rem;
.web-label {
margin-block-start: 0.25rem;
}
}
h4 {

View File

@@ -108,10 +108,6 @@
display: flex;
align-items: center;
gap: 0.75rem;
.web-label {
margin-block-start: 0.25rem;
}
}
h4 {

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { flip } from '$lib/utils/flip';
import { crossfade, scale, slide } from 'svelte/transition';
import { scale, slide } from 'svelte/transition';
import { functionsController } from '.';
const { state } = functionsController;

View File

@@ -1,4 +1,4 @@
import { safeAnimate, sleep } from '$lib/animations';
import { safeAnimate } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { animate } from 'motion';
import { getElSelector } from '../Products.svelte';

View File

@@ -231,6 +231,20 @@ export function write(text: string, cb: (v: string) => void, duration = 500) {
});
}
export function unwrite(text: string, cb: (v: string) => void, duration = 500) {
const step = duration / text.length;
let i = text.length;
return new Promise((resolve) => {
const interval = setInterval(() => {
cb(text.slice(0, --i));
if (i === 0) {
clearInterval(interval);
resolve(undefined);
}
}, step);
});
}
export function sleep(duration: number) {
return new Promise((resolve) => {
setTimeout(resolve, duration);

View File

@@ -8,12 +8,12 @@
<summary
class="collapsible-button flex cursor-pointer list-none appearance-none items-center justify-between marker:hidden [&::-webkit-details-marker]:hidden"
>
<span class="text">{title}</span>
<span class="text-primary text-sub-body font-medium">{title}</span>
<div class="icon text-primary transition-transform group-[&[open]]:rotate-180">
<span class="icon-cheveron-down" aria-hidden="true" />
</div>
</summary>
<div class="collapsible-content flex flex-col">
<div class="collapsible-content text-secondary text-sub-body flex flex-col">
<slot />
</div>
</details>

View File

@@ -0,0 +1,83 @@
<script lang="ts">
import { fade, scale } from 'svelte/transition';
import { trackEvent } from '$lib/actions/analytics';
let show = false;
function handleKeypress(event: KeyboardEvent) {
if (event.key.toLowerCase() === 'escape' || event.key.toLowerCase() === 'esc') {
event.preventDefault();
show = false;
}
}
</script>
<svelte:window on:keydown={handleKeypress} />
<button
on:click={() => {
show = true;
trackEvent('Appwrite in 100 seconds');
}}
class="web-button is-secondary cursor-pointer"
>
<span class="web-icon-play" style:color="unset" />
<span>Appwrite in 100 seconds</span>
</button>
{#if show}
<!-- `on:keypress={null}` silences the a11y warnings -->
<div
tabindex="0"
role="button"
class="overlay"
on:keypress={null}
on:click={() => (show = false)}
transition:fade={{ duration: 150 }}
/>
<div class="web-media content" transition:scale={{ duration: 250, start: 0.95 }}>
<iframe
src="https://www.youtube-nocookie.com/embed/L07xPMyL8sY?si=Odrwj1tHzlm12Fi2&controls=0"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
/>
</div>
{/if}
<style lang="scss">
.overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
transition: 200ms ease;
}
.content {
position: fixed;
left: 50%;
top: 50%;
translate: -50% -50%;
display: block;
object-fit: contain;
max-height: 75vh;
width: calc(80%);
aspect-ratio: 16 / 9;
z-index: 1000;
transform: scale(0.975);
transition: 200ms ease;
iframe {
display: block;
inline-size: 100%;
block-size: 100%;
}
}
</style>

View File

@@ -4,27 +4,17 @@
export let label: string = 'Get started';
</script>
<div class="call-to-action">
<div class="details">
<div
class="bg relative mt-12 !-mb-28 flex min-h-[12rem] items-center justify-center overflow-hidden border-t border-[hsl(var(--web-color-subtle))] py-12 lg:!-mb-[184px]"
>
<div class="flex max-w-3xs flex-col items-center justify-center gap-5 text-center">
<h2 class="text-label">{heading}</h2>
<a href={url} class="web-button">{label}</a>
</div>
</div>
<style lang="scss">
.call-to-action {
border-top: 1px solid hsl(var(--web-color-subtle));
padding: 48px 0;
min-height: 180px;
margin-top: 48px;
display: flex;
margin-bottom: -24px;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
.bg {
&::before {
content: '';
position: absolute;
@@ -36,22 +26,6 @@
rgba(253, 54, 110, 0.09),
transparent 85%
);
//filter: blur(10px);
}
.details {
gap: 20px;
max-width: 250px;
color: #fff;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
h2 {
margin: 0;
}
}
}
</style>

View File

@@ -1,5 +1,8 @@
<script lang="ts">
import { page } from '$app/stores';
import { fade } from 'svelte/transition';
import { loggedIn, user } from '$lib/utils/console';
import { PUBLIC_GROWTH_ENDPOINT } from '$env/static/public';
export let date: string | undefined = undefined;
let showFeedback = false;
@@ -13,7 +16,10 @@
async function handleSubmit() {
submitting = true;
error = undefined;
const response = await fetch('https://growth.appwrite.io/v1/feedback/docs', {
const userId = loggedIn && $user?.$id ? $user.$id : undefined;
const response = await fetch(`${PUBLIC_GROWTH_ENDPOINT}/feedback/docs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -22,7 +28,10 @@
email,
type: feedbackType,
route: $page.route.id,
comment
comment,
metaFields: {
userId
}
})
});
submitting = false;
@@ -32,6 +41,7 @@
}
comment = email = '';
submitted = true;
setTimeout(() => (showFeedback = false), 500);
}
function reset() {
@@ -44,6 +54,10 @@
$: if (!showFeedback) {
reset();
}
$: if (showFeedback && loggedIn && $user?.email) {
email = $user?.email;
}
</script>
<section class="web-content-footer">
@@ -59,7 +73,7 @@
class="web-radio-button"
aria-label="helpful"
on:click={() => {
showFeedback = feedbackType === 'positive' ? false : true;
showFeedback = feedbackType !== 'positive';
feedbackType = 'positive';
}}
>
@@ -69,7 +83,7 @@
class="web-radio-button"
aria-label="unhelpful"
on:click={() => {
showFeedback = feedbackType === 'negative' ? false : true;
showFeedback = feedbackType !== 'negative';
feedbackType = 'negative';
}}
>
@@ -103,6 +117,7 @@
on:submit|preventDefault={handleSubmit}
class="web-card is-normal"
style="--card-padding:1rem"
out:fade={{ duration: 450 }}
>
<div class="flex flex-col gap-2">
<label for="message">

View File

@@ -32,11 +32,11 @@
{ label: 'Solid', href: '/docs/quick-starts/solid' }
],
Products: [
{ label: 'Auth', href: '/docs/products/auth' },
{ label: 'Auth', href: '/products/auth' },
{ label: 'Databases', href: '/docs/products/databases' },
{ label: 'Functions', href: '/docs/products/functions' },
{ label: 'Functions', href: '/products/functions' },
{ label: 'Messaging', href: '/products/messaging' },
{ label: 'Storage', href: '/docs/products/storage' },
{ label: 'Storage', href: '/products/storage' },
{ label: 'Realtime', href: '/docs/apis/realtime' }
],
Learn: [

View File

@@ -1,16 +1,26 @@
<script lang="ts">
import Button from './ui/Button.svelte';
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
import { classNames } from '$lib/utils/classnames';
import { trackEvent } from '$lib/actions/analytics';
import { browser } from '$app/environment';
export let classes = '';
function getTrackingEventName() {
return browser
? 'loggedIn' in document.body.dataset
? 'Go to console'
: 'Get started'
: 'Get started';
}
</script>
<a
class={classNames('web-button web-u-inline-width-100-percent-mobile', classes)}
href={PUBLIC_APPWRITE_DASHBOARD}
on:click={() => trackEvent('Get started/go to console in header')}
on:click={() => {
trackEvent(`${getTrackingEventName()} in header`);
}}
>
<span class="hidden group-[&[data-logged-in]]/body:block">Go to Console</span>
<span class="block group-[&[data-logged-in]]/body:hidden">Get started</span>

View File

@@ -87,7 +87,7 @@
<ul
class="web-u-padding-block-start-80 grid grid-cols-3 text-center md:grid-cols-6 md:gap-10"
>
{#each logos as { src, alt, width, height }, i}
{#each logos as { src, alt, width, height }}
<li class="grid place-content-center">
<img {src} {alt} {width} {height} />
</li>

View File

@@ -92,6 +92,13 @@
display: grid;
}
@media #{devices.$break1} {
.status {
height: 55px;
margin-bottom: 6px; /* balancing due to style:margin-top="-4px" & the `iframe` has some spacings too I think */
}
}
.e-main-footer {
display: flex;
@media #{devices.$break1} {

View File

@@ -0,0 +1,71 @@
<script lang="ts" context="module">
export type NavLink = {
label: string;
href?: string;
showBadge?: boolean;
submenu?: ComponentType;
mobileSubmenu?: ComponentType;
};
</script>
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
import type { ComponentType } from 'svelte';
export let initialized = false;
export let links: NavLink[] = [];
</script>
<nav class="web-main-header-nav" aria-label="Main">
<ul class="web-main-header-nav-list flex items-center">
{#each links as link}
<li class="web-main-header-nav-item text-primary hover:text-accent">
{#if link.submenu}
<div
class="web-main-header-nav-item-button"
aria-haspopup="true"
aria-expanded="false"
aria-controls="submenu"
data-submenu-button
>
<svelte:component this={link.submenu} label={link.label} />
</div>
{:else}
<a
class={classNames(
'data-[badge]:after:animate-scale-in data-[badge]:relative data-[badge]:after:absolute data-[badge]:after:size-1.5 data-[badge]:after:translate-full data-[badge]:after:rounded-full'
)}
href={link.href}
data-initialized={initialized ? '' : undefined}
data-badge={link.showBadge ? '' : undefined}
>{link.label}
</a>
{/if}
</li>
{/each}
</ul>
</nav>
<style>
[data-badge] {
position: relative;
&::after {
content: '';
position: absolute;
background-color: hsl(var(--color-accent));
border-radius: 100%;
width: 0.375rem;
height: 0.375rem;
inset-block-start: -2px;
inset-inline-end: -4px;
translate: 100%;
}
&:not([data-initialized])::after {
animation: scale-in 0.2s ease-out;
}
}
</style>

View File

@@ -2,7 +2,7 @@
import { afterNavigate } from '$app/navigation';
import { IsLoggedIn } from '$lib/components';
import { GITHUB_REPO_LINK, GITHUB_STARS } from '$lib/constants';
import type { NavLink } from '$lib/layouts/Main.svelte';
import type { NavLink } from './MainNav.svelte';
export let open = false;
export let links: NavLink[];
@@ -16,7 +16,7 @@
<nav class="web-side-nav web-is-not-desktop" class:hidden={!open}>
<div class="web-side-nav-wrapper ps-4 pe-4">
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 px-4">
<a href="https://cloud.appwrite.io/register" class="web-button is-secondary flex-1">
Sign up
</a>
@@ -25,11 +25,15 @@
<div class="web-side-nav-scroll">
<section>
<ul>
{#each links as { href, label }}
{#each links as { href, label, mobileSubmenu }}
<li>
<a class="web-side-nav-button" {href}>
<span class="text-caption">{label}</span>
</a>
{#if mobileSubmenu}
<svelte:component this={mobileSubmenu} {label} />
{:else}
<a class="web-side-nav-button" {href}>
<span class="text-caption">{label}</span>
</a>
{/if}
</li>
{/each}
</ul>

View File

@@ -0,0 +1,95 @@
<script lang="ts">
import { platformMap } from '$lib/utils/references';
import { writable } from 'svelte/store';
import { getCodeHtml, type Language } from '$lib/utils/code';
import { copy } from '$lib/utils/copy';
import { Select, Tooltip } from '$lib/components';
export let selected: Language = 'js';
export let data: { language: string; content: string; platform?: string }[] = [];
export let width: number | null = null;
export let height: number | null = null;
$: snippets = writable(new Set(data.map((d) => d.language)));
$: content = data.find((d) => d.language === selected)?.content ?? '';
$: platform = data.find((d) => d.language === selected)?.platform ?? '';
snippets?.subscribe((n) => {
if (selected === null && n.size > 0) {
selected = Array.from(n)[0] as Language;
}
});
enum CopyStatus {
Copy = 'Copy',
Copied = 'Copied!'
}
let copyText = CopyStatus.Copy;
async function handleCopy() {
await copy(content);
copyText = CopyStatus.Copied;
setTimeout(() => {
copyText = CopyStatus.Copy;
}, 1000);
}
$: result = getCodeHtml({
content,
language: selected ?? 'sh',
withLineNumbers: true
});
$: options = Array.from($snippets).map((language) => ({
value: language,
label: platformMap[language]
}));
</script>
<section
class="dark web-code-snippet mx-auto lg:!max-w-[90vw]"
aria-label="code-snippet panel"
style={`width: ${width ? width / 16 + 'rem' : 'inherit'}; height: ${
height ? height / 16 + 'rem' : 'inherit'
}`}
>
<header class="web-code-snippet-header">
<div class="web-code-snippet-header-start">
<div class="flex gap-4">
{#if platform}
<div class="web-tag"><span class="text">{platform}</span></div>
{/if}
</div>
</div>
<div class="web-code-snippet-header-end">
<ul class="buttons-list flex gap-3">
{#if $snippets.entries.length}
<li class="buttons-list-item flex self-center">
<Select bind:value={selected} bind:options />
</li>
{/if}
<li class="buttons-list-item" style="padding-inline-start: 13px">
<Tooltip>
<button
on:click={handleCopy}
class="web-icon-button"
aria-label="copy code from code-snippet"
><span class="web-icon-copy" aria-hidden="true" /></button
>
<svelte:fragment slot="tooltip">
{copyText}
</svelte:fragment>
</Tooltip>
</li>
</ul>
</div>
</header>
<div
class="web-code-snippet-content"
style={`height: ${height ? height / 16 + 'rem' : 'inherit'}`}
>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html result}
</div>
</section>

View File

@@ -1,14 +1,15 @@
<script context="module" lang="ts">
import { PUBLIC_GROWTH_ENDPOINT } from '$env/static/public';
export async function newsletter(name: string, email: string) {
const response = await fetch('https://growth.appwrite.io/v1/newsletter/subscribe', {
const response = await fetch(`${PUBLIC_GROWTH_ENDPOINT}/newsletter/subscribe`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name,
email,
cloud: true /* not optional on the growth endpoint. */
email
})
});
return response;

View File

@@ -3,7 +3,13 @@
import { trackEvent } from '$lib/actions/analytics';
</script>
<img src="/images/bgs/pre-footer.png" alt="" class="web-pre-footer-bg" style="z-index:-1" />
<img
src="/images/bgs/pre-footer.png"
alt=""
class="web-pre-footer-bg"
loading="lazy"
style="z-index:-1"
/>
<div class="web-u-row-gap-80 relative grid gap-8 md:grid-cols-2">
<section class="web-hero flex items-center justify-center gap-y-8">
@@ -152,5 +158,6 @@
height: auto;
max-inline-size: unset;
max-block-size: unset;
filter: blur(100px);
}
</style>

View File

@@ -0,0 +1,87 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
import { melt, createCollapsible } from '@melt-ui/svelte';
import { slide } from 'svelte/transition';
import { products, sublinks } from './ProductsSubmenu.svelte';
import { dev } from '$app/environment';
export let label: string;
const {
elements: { root, content, trigger },
states: { open }
} = createCollapsible();
</script>
<div use:melt={$root} class="relative mx-auto block md:hidden">
<div class="flex items-center justify-between">
<button
use:melt={$trigger}
class="text-caption web-side-nav-button flex items-center justify-between"
>{label}
<span
class={classNames('web-icon-chevron-down transition-transform', {
'rotate-180': $open
})}
/></button
>
</div>
<div>
{#if $open}
<div use:melt={$content} transition:slide class="py-3 px-4">
<div class="flex flex-col gap-2">
{#each products as product}
<a
href={product.href}
class="group flex gap-3 rounded-xl p-2 text-white outline-none transition-colors focus:bg-white/8"
>
<div
class="flex size-12 shrink-0 items-center justify-center rounded-lg border border-white/12 bg-white/6"
>
<img
src={product.icon}
alt={product.name}
class="size-6 grayscale transition-all group-focus:grayscale-0"
/>
</div>
<div class="">
<span class="text-sub-body text-primary font-medium"
>{product.name}
{#if product.beta}
<span
class="text-caption bg-accent/24 ml-1 rounded py-1 px-2 font-medium text-white"
>Coming soon</span
>
{/if}
</span>
<p class="text-caption text-secondary text-pretty">
{product.description}
</p>
</div>
</a>
{/each}
</div>
{#if dev}
<div class="mt-8">
<span
class="font-aeonik-fono tracking-loose text-secondary block text-xs uppercase"
>This is a title<span class="text-accent">_</span></span
>
<div class="mt-3 space-y-3">
{#each sublinks as sublink}
<a
href={sublink.href}
class="text-caption text-primary flex items-center gap-2"
>
{sublink.label} <span class="web-icon-chevron-right" />
</a>
{/each}
</div>
</div>
{/if}
</div>
{/if}
</div>
</div>

View File

@@ -0,0 +1,209 @@
<script lang="ts" context="module">
export type SubmenuItem = {
name: string;
href: string;
description: string;
icon: string;
beta?: boolean;
};
export type SubLink = {
label: string;
href: string;
};
export const products: Array<SubmenuItem> = [
{
name: 'Auth',
href: '/products/auth',
description: 'Secure login with multi-factor auth.',
icon: '/images/icons/illustrated/dark/auth.png'
},
{
name: 'Databases',
href: '/docs/products/databases',
description: 'Scalable and robust databases.',
icon: '/images/icons/illustrated/dark/databases.png'
},
{
name: 'Storage',
href: '/products/storage',
description: 'Advanced compression and encryption.',
icon: '/images/icons/illustrated/dark/storage.png'
},
{
name: 'Functions',
href: '/products/functions',
description: 'Deploy & scale serverless functions.',
icon: '/images/icons/illustrated/dark/functions.png'
},
{
name: 'Messaging',
href: '/products/messaging',
description: 'Set up a full-functioning messaging service.',
icon: '/images/icons/illustrated/dark/messaging.png'
},
{
name: 'Realtime',
href: '/docs/apis/realtime',
description: 'Subscribe and react to any event.',
icon: '/images/icons/illustrated/dark/realtime.png'
}
];
export const sublinks: Array<SubLink> = [
{
label: 'See more',
href: '/'
},
{
label: 'See more',
href: '/'
},
{
label: 'See more',
href: '/'
}
];
</script>
<script lang="ts">
import { dev } from '$app/environment';
import { classNames } from '$lib/utils/classnames';
import { createDropdownMenu, melt } from '@melt-ui/svelte';
import { trackEvent } from '$lib/actions/analytics';
const {
elements: { trigger, menu, item, overlay },
states: { open }
} = createDropdownMenu({
loop: true
});
export let label: string;
</script>
<button
class={classNames(
'text-primary focus:text-accent hover:text-accent inline-flex cursor-pointer items-center justify-between outline-none',
{
'text-accent': $open
}
)}
use:melt={$trigger}
>
{label}
<span
class={classNames('web-icon-chevron-down block transition-transform', {
'rotate-180': $open
})}
/>
</button>
<div
use:melt={$menu}
class={classNames(
'data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in relative !left-1/2 z-10 mt-6 mx-auto hidden w-full -translate-x-1/2 flex-col items-center p-0 outline-none [max-inline-size:86.875rem] md:flex'
)}
>
<div class="is-special-padding w-full rounded-2xl border border-white/8 bg-[#232325] p-6">
<div class="grid w-full grid-cols-1 place-content-between gap-16 lg:grid-cols-12">
<div class="col-span-8 -mr-12 pr-12">
<span
class="font-aeonik-fono text-secondary tracking-loose mb-4 block text-xs uppercase"
>{label}<span class="text-accent">_</span></span
>
<div
class="grid grid-flow-col-dense grid-cols-1 gap-2 md:grid-cols-2 md:grid-rows-4"
>
{#each products as product}
<a
href={product.href}
use:melt={$item}
on:click={() => trackEvent(`${product.name} in products submenu`)}
class="group flex gap-3 rounded-xl p-1 text-white outline-none transition-colors focus:bg-white/8"
>
<div
class="flex size-12 shrink-0 items-center justify-center rounded-lg border border-white/12 bg-white/6"
>
<img
src={product.icon}
alt={product.name}
class="size-6 grayscale transition-all group-focus:grayscale-0"
/>
</div>
<div class="">
<span class="text-sub-body text-primary font-medium"
>{product.name}
{#if product.beta}
<span
class="text-caption bg-accent/24 ml-1 rounded py-1 px-2 font-medium text-white"
>Coming soon</span
>
{/if}
</span>
<p class="text-caption text-secondary text-pretty">
{product.description}
</p>
</div>
</a>
{/each}
</div>
</div>
<div class="col-span-4 -ml-12 border-l border-white/6 pl-12">
<a
href="/blog/post/case-study-undo"
use:melt={$item}
class="block rounded-2xl border border-white/12 bg-white/6 p-4 outline-none focus-within:bg-white/12"
>
<header class="flex items-center justify-between">
<span
class="font-aeonik-fono tracking-loose text-secondary block text-xs uppercase"
>Customer stories<span class="text-accent">_</span></span
>
<a
href="/blog/category/case-studies"
class="text-primary text-caption flex items-center gap-2"
>See more <span class="web-icon-chevron-right" /></a
>
</header>
<div class="flex-1 outline-none">
<img
src="/images/blog/case-study-undo/cover.png"
alt="Case study cover"
class="my-6 aspect-[3/1] rounded-xl object-cover"
/>
<p>
Pioneering asset management solutions for the circular economy with UNDŌ
</p>
</div>
</a>
{#if dev}
<div class="mt-8">
<span
class="font-aeonik-fono tracking-loose text-secondary block text-xs uppercase"
>This is a title<span class="text-accent">_</span></span
>
<div class="mt-3 space-y-3">
{#each sublinks as sublink}
<a
href={sublink.href}
class="text-caption text-primary flex items-center gap-2"
>
{sublink.label} <span class="web-icon-chevron-right" />
</a>
{/each}
</div>
</div>
{/if}
</div>
</div>
</div>
<div
use:melt={$overlay}
class="data-[state=closed]:animate-fade-out fixed inset-0 bg-black/60"
/>
</div>

View File

@@ -26,6 +26,8 @@
change: unknown;
}>();
export let initialLabel: string = 'Select an option';
const {
elements: { trigger, menu, option: optionEl, group: groupEl, groupLabel },
states: { open, selected, selectedLabel }
@@ -80,6 +82,8 @@
duration: 150,
y: placement === 'top' ? 4 : -4
} as FlyParams;
console.log({ initialLabel, $selectedLabel });
</script>
<button
@@ -93,7 +97,7 @@
{#if selectedOption?.icon}
<span class={selectedOption.icon} aria-hidden="true" />
{/if}
<span>{$selectedLabel}</span>
<span>{$selectedLabel || initialLabel}</span>
</div>
<span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true" />
</button>
@@ -115,7 +119,7 @@
{#if option.icon}
<span class={option.icon} aria-hidden="true" />
{/if}
<span style:text-transform="capitalize">{option.label}</span>
<span>{option.label}</span>
</button>
{/each}
</div>

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import { getTocCtx } from './TocRoot.svelte';
import TocTree from './TocTree.svelte';
import { cubicOut } from 'svelte/easing';
import { getTocCtx } from './TocRoot.svelte';
import { onMount } from 'svelte';
export let showToc = true;
@@ -12,94 +14,214 @@
} = getTocCtx();
$: progress = Math.max(...$activeHeadingIdxs) / ($headingsTree.length - 1);
function slideFade(
node: HTMLElement,
{
delay = 0,
duration = 400,
easing = cubicOut
}: { delay?: number; duration?: number; easing?: (t: number) => number } = {}
) {
const initialHeight = node.offsetHeight;
return {
delay,
duration,
easing,
css: (t: number) => {
return `
opacity: ${t};
height: ${t * initialHeight}px;
overflow: hidden;
`;
}
};
}
// show toc by default on desktop
function handleResizeForTocTree() {
showToc = window.innerWidth >= 1024;
}
onMount(handleResizeForTocTree);
</script>
<aside class="web-grid-120-1fr-auto-side" class:web-is-mobile-closed={!showToc}>
<div class="web-page-steps">
<div
class="web-page-steps-location web-is-not-mobile"
style="--location:{progress * 100}%;"
<svelte:window on:resize={handleResizeForTocTree} />
<section class="web-mobile-header">
<div class="web-is-only-mobile">
<button
on:click={() => (showToc = !showToc)}
class="flex w-full items-center justify-between"
>
<span class="web-page-steps-location-button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<g clip-path="url(#clip0_1684_10747)">
<g filter="url(#filter0_b_1684_10747)">
<circle
cx="8"
cy="8"
r="8"
fill="url(#paint0_linear_1684_10747)"
fill-opacity="0.32"
/>
<circle
cx="8"
cy="8"
r="7.75"
stroke="url(#paint1_linear_1684_10747)"
stroke-width="0.5"
/>
</g>
<circle cx="8" cy="7.99219" r="3" fill="white" />
</g>
<defs>
<filter
id="filter0_b_1684_10747"
x="-200"
y="-200"
width="416"
height="416"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="100" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_1684_10747"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_1684_10747"
result="shape"
/>
</filter>
<linearGradient
id="paint0_linear_1684_10747"
x1="2.02105"
y1="1.10843"
x2="16.3872"
y2="17.2901"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="white" stop-opacity="0.4" />
<stop offset="1" stop-color="white" stop-opacity="0" />
</linearGradient>
<linearGradient
id="paint1_linear_1684_10747"
x1="7.45643"
y1="-1.10615"
x2="5.53812"
y2="17.9973"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="white" stop-opacity="0.16" />
<stop offset="1" stop-color="white" stop-opacity="0" />
</linearGradient>
<clipPath id="clip0_1684_10747">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
<span class="flex w-full items-center justify-between">
<span class="text-description">Table of contents</span>
<span
aria-hidden="true"
class="toggle-icon {showToc ? 'web-icon-close' : 'icon-menu-alt-4'}"
></span>
</span>
</div>
<TocTree tree={$headingsTree} activeHeadingIdxs={$activeHeadingIdxs} {item} />
</button>
</div>
</aside>
{#if showToc}
<aside
class="web-grid-120-1fr-auto-side"
class:web-is-mobile-closed={!showToc}
transition:slideFade={{ duration: 300 }}
>
<div class="web-page-steps">
<div
class="web-page-steps-location web-is-not-mobile"
style="--location:{progress * 100}%;"
>
<span class="web-page-steps-location-button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<g clip-path="url(#clip0_1684_10747)">
<g filter="url(#filter0_b_1684_10747)">
<circle
cx="8"
cy="8"
r="8"
fill="url(#paint0_linear_1684_10747)"
fill-opacity="0.32"
/>
<circle
cx="8"
cy="8"
r="7.75"
stroke="url(#paint1_linear_1684_10747)"
stroke-width="0.5"
/>
</g>
<circle cx="8" cy="7.99219" r="3" fill="white" />
</g>
<defs>
<filter
id="filter0_b_1684_10747"
x="-200"
y="-200"
width="416"
height="416"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="100" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_1684_10747"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_1684_10747"
result="shape"
/>
</filter>
<linearGradient
id="paint0_linear_1684_10747"
x1="2.02105"
y1="1.10843"
x2="16.3872"
y2="17.2901"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="white" stop-opacity="0.4" />
<stop offset="1" stop-color="white" stop-opacity="0" />
</linearGradient>
<linearGradient
id="paint1_linear_1684_10747"
x1="7.45643"
y1="-1.10615"
x2="5.53812"
y2="17.9973"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="white" stop-opacity="0.16" />
<stop offset="1" stop-color="white" stop-opacity="0" />
</linearGradient>
<clipPath id="clip0_1684_10747">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
</span>
</div>
<div class="toc-tree-holder">
<TocTree
tree={$headingsTree}
activeHeadingIdxs={$activeHeadingIdxs}
{item}
bind:showToc
/>
</div>
</div>
</aside>
{/if}
</section>
<style>
.web-mobile-header {
top: 5rem;
grid-area: side;
background: unset;
max-height: fit-content;
border-block-end: unset;
border-block-start: unset;
}
@media (max-width: 1024px) {
.web-mobile-header {
top: 0;
margin: 1rem 0;
display: block;
position: sticky;
padding: 1.375rem 0;
align-content: center;
/** 1.5rem covers main header completely so fragments of it are not shown during scroll */
padding-block: 1.5rem;
padding-inline: 1.25rem;
background: hsl(var(--p-body-bg-color));
border-block-end: solid 1px var(--p-mobile-header-border-color);
border-block-start: solid 1px var(--p-mobile-header-border-color);
}
.toc-tree-holder {
margin-top: 1.5rem;
margin-left: 0.25rem;
}
.web-icon-close {
max-width: 20px;
max-height: 24px;
}
}
@media (min-width: 1280px) {
.web-mobile-header {
top: 7rem;
display: block;
}
}
@media (min-width: 1024px) {
.web-mobile-header {
border-block-end: unset;
border-block-start: unset;
}
.toc-tree-holder {
margin-top: unset;
}
}
</style>

View File

@@ -1,17 +1,25 @@
<script lang="ts">
import { type TableOfContentsItem, type TableOfContentsElements, melt } from '@melt-ui/svelte';
import { getTocCtx } from './TocRoot.svelte';
import { browser } from '$app/environment';
export let tree: TableOfContentsItem[] = [];
export let activeHeadingIdxs: number[];
export let item: TableOfContentsElements['item'];
export let level = 1;
export let showToc = true;
const {
toc: {
helpers: { isActive }
}
} = getTocCtx();
function onItemClick() {
const isDesktop = browser ? window.innerWidth >= 1024 : false;
if (!isDesktop) showToc = !showToc;
}
</script>
<ul class="web-page-steps-list text-sub-body font-medium">
@@ -22,6 +30,7 @@
class:is-selected={$isActive(heading.id)}
href="#{heading.id}"
use:melt={$item(heading.id)}
on:click|preventDefault={onItemClick}
>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html heading.node.innerHTML}

View File

@@ -7,10 +7,16 @@
type EmblaPluginType
} from 'embla-carousel';
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
import AutoScrollPlugin, { type AutoScrollOptionsType } from 'embla-carousel-auto-scroll';
let emblaApi: EmblaCarouselType;
let options: EmblaOptionsType = {
export let showBullets: boolean = true;
export let showArrows: boolean = true;
export let autoScrollOptions: AutoScrollOptionsType = {
active: false
};
export let options: EmblaOptionsType = {
align: 'center',
skipSnaps: true,
loop: true
@@ -27,7 +33,7 @@
else hasNext = false;
};
let plugins: EmblaPluginType[] = [WheelGesturesPlugin()];
let plugins: EmblaPluginType[] = [WheelGesturesPlugin(), AutoScrollPlugin(autoScrollOptions)];
let selectedScrollIndex = 0;
const onSelect = (index: number) => {
@@ -123,15 +129,17 @@
</script>
<div class="embla web-carousel relative overflow-hidden">
{#if hasPrev}
<button class="web-carousel-button web-carousel-button-start" on:click={onPrev}>
<span class="web-icon-arrow-left" aria-hidden="true"></span>
</button>
{/if}
{#if hasNext}
<button class="web-carousel-button web-carousel-button-end" on:click={onNext}>
<span class="web-icon-arrow-right" aria-hidden="true"></span>
</button>
{#if showArrows}
{#if hasPrev}
<button class="web-carousel-button web-carousel-button-start" on:click={onPrev}>
<span class="web-icon-arrow-left" aria-hidden="true"></span>
</button>
{/if}
{#if hasNext}
<button class="web-carousel-button web-carousel-button-end" on:click={onNext}>
<span class="web-icon-arrow-right" aria-hidden="true"></span>
</button>
{/if}
{/if}
<div class="embla__viewport" use:embla={{ options, plugins }} on:emblaInit={onEmblaInit}>
@@ -139,19 +147,45 @@
<slot />
</ul>
</div>
<div class="shadow" />
</div>
<div class="web-carousel-bullets">
<ul class="web-carousel-bullets-list">
{#each Array.from({ length: emblaApi?.scrollSnapList().length }) as _, i}
<li class="web-carousel-bullets-item rounded-full">
<button
class="web-carousel-bullets-button"
class:is-selected={selectedScrollIndex === i}
aria-label={`gallery item ${i + 1}`}
on:click={() => onSelect(i)}
></button>
</li>
{/each}
</ul>
</div>
{#if showBullets}
<div class="web-carousel-bullets">
<ul class="web-carousel-bullets-list">
{#each Array.from({ length: emblaApi?.scrollSnapList().length }) as _, i}
<li class="web-carousel-bullets-item rounded-full">
<button
class="web-carousel-bullets-button"
class:is-selected={selectedScrollIndex === i}
aria-label={`gallery item ${i + 1}`}
on:click={() => onSelect(i)}
></button>
</li>
{/each}
</ul>
</div>
{/if}
<style>
.shadow {
width: 100vw;
height: 80vh;
position: absolute;
top: 50%;
left: 50%;
pointer-events: none;
transform: translateX(-50%) translateY(-50%);
z-index: 10;
backdrop-filter: blur(2px);
background-color: hsl(var(--web-color-background) / 50%);
mask-composite: intersect;
mask-image: linear-gradient(
to right,
rgba(0, 0, 0, 1) 0%,
transparent,
transparent,
rgba(0, 0, 0, 1) 100%
);
}
</style>

View File

@@ -1,25 +1,36 @@
<li class="slide web-carousel-item">
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
let className = '';
export { className as class };
</script>
<li
class={classNames(
'slide web-carousel-item mr-2 flex-[0_0_100%] cursor-grab active:cursor-grabbing md:flex-[0_0_50%]',
className
)}
>
<div class="embla__slide__number">
<slot />
</div>
</li>
<style lang="scss">
@use '$scss/abstract' as *;
@use '$scss/abstract/functions' as f;
.slide {
cursor: grab;
&:active {
cursor: grabbing;
}
flex: 0 0 50%;
min-width: 0;
margin-right: f.pxToRem(16);
@media (max-width: 768px) {
flex: 0 0 100%;
}
flex: 0 0 50%;
min-width: 0;
margin-right: pxToRem(16);
&:active {
cursor: grabbing;
}
}
</style>

View File

@@ -0,0 +1,78 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
type $$Props = {
eyebrow: {
label: string;
icon: string;
};
title: string;
description: string;
cta: {
label: string;
url: string;
};
secondaryCta?: {
label: string;
url: string;
};
image: {
url: string;
alt?: string;
};
mobileImage?: {
url: string;
alt?: string;
};
};
export let eyebrow: $$Props['eyebrow'];
export let title: $$Props['title'];
export let description: $$Props['description'];
export let cta: $$Props['cta'];
export let secondaryCta: $$Props['secondaryCta'] = undefined;
export let image: $$Props['image'];
export let mobileImage: $$Props['mobileImage'] = undefined;
</script>
<div
class="border-smooth box-content flex items-center border-b bg-[url(/images/bgs/mobile-hero.png)] bg-cover bg-bottom py-12 px-5 md:bg-[url(/images/bgs/hero.png)] md:bg-center md:pt-32 md:pb-40 lg:px-8 xl:px-16"
>
<div class="mx-auto grid max-w-[75rem] items-center gap-16 md:grid-cols-2">
<div class="space-y-8">
<div class="flex items-center gap-2">
<img src={eyebrow.icon} class="size-8" alt="" />
<span class="text-micro font-aeonik-fono tracking-loose text-primary uppercase">
{eyebrow.label}<span class="web-u-color-text-accent">_</span>
</span>
</div>
<h1
class="text-display font-aeonik-pro text-primary text-pretty max-sm:max-w-[300px] md:max-w-md"
>
{title}
</h1>
<p class="text-description text-secondary text-pretty font-medium">
{description}
</p>
<div class="flex flex-col items-center gap-2 md:flex-row">
<a href={cta.url} class="web-button !w-full md:!w-fit">
{cta.label}
</a>
{#if secondaryCta}
<a href={secondaryCta.url} class="web-button is-secondary !w-full md:!w-fit">
{secondaryCta.label}
</a>
{/if}
</div>
</div>
<img
class={classNames({ 'hidden md:block': mobileImage })}
src={image.url}
alt={image.alt ?? ''}
/>
{#if mobileImage}
<img class="block md:hidden" src={mobileImage.url} alt={mobileImage.alt ?? ''} />
{/if}
</div>
</div>

View File

@@ -0,0 +1,75 @@
<script lang="ts">
const allProducts = {
messaging: {
title: 'Messaging',
description: 'Use Appwrite messaging to send email, SMS, and push notifications.',
icon: '/images/icons/illustrated/dark/messaging.png',
url: '/docs/products/messaging'
},
auth: {
title: 'Auth',
description: 'Build secure authentication and manage your users.',
icon: '/images/icons/illustrated/dark/auth.png',
url: '/docs/products/auth'
},
functions: {
title: 'Functions',
description: ' Scale big and unlock limitless potential with Appwrite functions.',
icon: '/images/icons/illustrated/dark/functions.png',
url: '/docs/products/functions'
},
databases: {
title: 'Databases',
description: 'Store and query structured data, ensuring scalable storage.',
icon: '/images/icons/illustrated/dark/databases.png',
url: '/docs/products/databases'
},
storage: {
title: 'Storage',
description: 'Manage your files project, using convenient APIs and utilities.',
icon: '/images/icons/illustrated/dark/storage.png',
url: '/docs/products/storage'
}
} as const;
export let exclude:
| 'messaging'
| 'functions'
| 'databases'
| 'storage'
| 'auth'
| 'realtime'
| undefined = undefined;
const products = Object.entries(allProducts)
.filter(([key]) => key !== exclude)
.map(([_, value]) => value);
</script>
<section class="border-smooth border-t py-20 md:py-40">
<div class="container">
<h4 class="text-label text-primary text-center">Keep exploring our products</h4>
<div class="mt-8 grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
{#each products as product}
<a
class="web-card is-normal"
href={product.url}
style="background: rgba(255, 255, 255, 0.04);"
>
<div
class="web-u-padding-inline-8 web-u-padding-block-end-8 flex flex-col gap-2"
>
<div class="flex items-center gap-2">
<img src={product.icon} alt="auth" width="32" height="32" />
<h4 class="text-body text-primary">{product.title}</h4>
<span class="web-icon-arrow-right ml-auto" aria-hidden="true" />
</div>
<p class="text-sub-body">
{product.description}
</p>
</div>
</a>
{/each}
</div>
</div>
</section>

View File

@@ -0,0 +1,74 @@
<script lang="ts">
const testimonials = [
{
name: 'Ryan OConner',
copy: `The switch to using Appwrite brought infinite value that I'm still discovering today, but a major impact that it made was the amount of time and stress that it saved me as it simply just works.`,
image: '/images/testimonials/ryan-oconner.png',
title: 'Founder',
company: 'K-Collect'
},
{
name: 'David Forster',
copy: `We really loved working with Appwrite for launching our bootstrapped "Open Mind" App. I am still surprised how easy the implementation into Flutter was.`,
image: '/images/testimonials/david-forster.png'
},
{
name: 'Marius Bolik',
copy: `The integrated user authentication and the ease of creating data structures have undoubtedly saved us several weeks' worth of time.`,
image: '/images/testimonials/marius-bolik2.png',
title: 'CTO',
company: 'mySHOEFITTER'
},
{
name: 'Sergio Ponguta',
copy: `Just go for it, dont think twice. Try Appwrite, and you will love it!`,
image: '/images/testimonials/smartbee.png',
title: 'Founder',
company: 'Smart Bee'
},
{
name: 'Phil McClusky',
copy: 'Just like a Swiss Army Knife, you can choose and use the tools that you need with Appwrite.',
image: '/images/testimonials/majik.png',
title: 'Developer',
company: 'Majik Kids'
}
];
</script>
<div class="relative w-full max-w-[100vw] overflow-hidden">
<div class="group light flex w-fit gap-4">
{#each Array.from({ length: 4 }) as _, i}
<div
class="animate-scroll flex items-center gap-8 group-hover:[animation-play-state:paused;]"
aria-hidden={i !== 0}
>
{#each testimonials as testimonial}
<div
class="flex h-fit w-[90vw] flex-col justify-center rounded-2xl bg-white p-6 transition-all md:w-lg"
>
<p class="text-sub-body text-secondary flex-1 font-medium">
{testimonial.copy}
</p>
<div class="mt-4 flex items-center gap-3">
<img
src={testimonial.image}
class="size-12 rounded-full"
alt="{testimonial.company} Logo"
/>
<div>
<span class="text-secondary text-sub-body block font-medium">
{testimonial.name}
</span>
<span class="text-sub-body text-secondary block"
>{testimonial.title} // {testimonial.company}</span
>
</div>
</div>
</div>
{/each}
</div>
{/each}
</div>
</div>

View File

@@ -1,3 +1,16 @@
<span class="block bg-[linear-gradient(6deg,_#f8a1ba,_#fff_35%)] bg-clip-text text-transparent">
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
let className = '';
export { className as class };
</script>
<span
class={classNames(
'block bg-[linear-gradient(6deg,_#f8a1ba,_#fff_35%)] bg-clip-text text-transparent',
className
)}
>
<slot />
</span>

View File

@@ -34,7 +34,9 @@
}
const CTX_KEY = Symbol('docs');
const TUT_CTX_KEY = Symbol('tut-docs');
export const isInDocs = () => getContext<boolean>(CTX_KEY) ?? false;
export const isInTutorialDocs = () => getContext<boolean>(TUT_CTX_KEY) ?? false;
</script>
<script lang="ts">
@@ -43,6 +45,7 @@
import { getContext, setContext } from 'svelte';
import { GITHUB_REPO_LINK, GITHUB_STARS } from '$lib/constants';
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
import { page } from '$app/stores';
export let variant: DocsLayoutVariant = 'default';
export let isReferences = false;
@@ -63,7 +66,9 @@
showSidenav: false
}));
});
setContext(CTX_KEY, true);
const key = $page.route.id?.includes('tutorials') ? TUT_CTX_KEY : CTX_KEY;
setContext(key, true);
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && ($layoutState.showReferences || $layoutState.showSidenav)) {

View File

@@ -13,14 +13,17 @@
</script>
<script lang="ts">
import { scrollToTop } from '$lib/actions/scrollToTop';
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
import { Feedback } from '$lib/components';
import { scrollToTop } from '$lib/actions/scrollToTop';
export let title: string;
export let toc: Array<TocItem>;
export let back: string | undefined = undefined;
export let date: string | undefined = undefined;
const reducedArticleSize = setContext('articleHasNumericBadge', writable(false));
</script>
<main class="contents" id="main">
@@ -59,8 +62,9 @@
</div>
<div class="web-article-header-end" />
</header>
<div class="web-article-content">
<div class="web-article-content" class:web-reduced-article-size={$reducedArticleSize}>
<slot />
<Feedback {date} />
</div>
<aside class="web-references-menu ps-6">
@@ -110,3 +114,12 @@
</aside>
</article>
</main>
<style>
@media (min-width: 1280px) and (max-width: 1330px) {
.web-reduced-article-size {
/* original/default is 41.5rem */
max-inline-size: 40.5rem;
}
}
</style>

View File

@@ -4,7 +4,8 @@
import type { Tutorial } from '$markdoc/layouts/Tutorial.svelte';
import type { TocItem } from './DocsArticle.svelte';
import Heading from '$markdoc/nodes/Heading.svelte';
import { onMount } from 'svelte';
import { onMount, tick } from 'svelte';
import { page } from '$app/stores';
export let toc: Array<TocItem>;
export let back: string;
@@ -31,13 +32,66 @@
let slotContent: HTMLElement | null = null;
function scrollToElement(pageHash: string) {
const element = document.getElementById(pageHash);
if (element) {
const offset = 50;
const rect = element.getBoundingClientRect();
window.scroll({ top: window.scrollY + rect.top - offset });
}
}
/**
* Due to underlying logic with anchor links & the auto-scroll via hash values in the URL,
* we have an issue where if the first item is not scrolled enough it isn't marked as `selected`.
*
* We do below workaround for the time being without breaking things to scroll to the first item.
*/
async function preSelectItemOnInit() {
await tick();
if (!$page.url.hash) return;
const tocItem = toc.slice(1);
// no sub-items, return.
if (!tocItem.length) return;
const pageHash = $page.url.hash.replace('#', '');
const tocItemHref = tocItem[0].href.replace('#', '');
if (pageHash !== tocItemHref) return;
scrollToElement(pageHash);
}
// same issue as above, only happens on the first item.
function scrollToItem(parent: TocItem, index: number) {
const tocItem = toc.slice(1);
if (!tocItem.length) return;
const tocItemHref = parent.href.replace('#', '');
const element = document.getElementById(tocItemHref);
if (index === 0) {
scrollToElement(tocItemHref);
} else {
element?.scrollIntoView();
}
// because we used `preventDefault`.
history.pushState(null, '', parent.href);
}
onMount(() => {
if (!slotContent) return;
// dynamically modify all `label` headers to `body`.
slotContent.querySelectorAll<HTMLHeadingElement>('h2.web-label').forEach((header) => {
header.classList.replace('web-label', 'web-main-body-500');
slotContent.querySelectorAll<HTMLHeadingElement>('h2.text-label').forEach((header) => {
header.classList.replace('text-label', 'web-main-body-500');
});
preSelectItemOnInit();
});
</script>
@@ -77,7 +131,9 @@
/>
</a>
{/if}
<h1 class="web-title lg:-ml-5">{firstStepItem?.title}</h1>
<h1 class="web-title {currentStep === 1 ? 'lg:-ml-5' : ''}">
{firstStepItem?.title}
</h1>
</div>
</div>
<div class="web-article-header-end" />
@@ -87,16 +143,18 @@
<section class="web-article-content-sub-section">
<header class="web-article-content-header">
<span class="web-numeric-badge">{currentStep}</span>
<Heading level={1} id={currentStepItem.href} step={currentStep}>
{getCorrectTitle(currentStepItem, 1)}
</Heading>
<div class="tutorial-heading">
<Heading level={1} id={currentStepItem.href} step={currentStep}>
{getCorrectTitle(currentStepItem, 1)}
</Heading>
</div>
</header>
<div class="u-padding-block-start-32" bind:this={slotContent}>
<div class="web-u-padding-block-start-32" bind:this={slotContent}>
<slot />
</div>
<div class="flex justify-between">
<div class="web-u-padding-block-start-32 flex justify-between">
{#if prevStep}
<a href={prevStep.href} class="web-button is-text previous-step-anchor">
<span class="icon-cheveron-left" aria-hidden="true" />
@@ -135,6 +193,7 @@
<ol class="web-references-menu-list">
{#each tutorials as tutorial, index}
{@const isCurrentStep = currentStep === tutorial.step}
{@const absoluteToc = toc.slice(1)}
<li class="web-references-menu-item">
<a
href={tutorial.href}
@@ -148,14 +207,16 @@
>{index === 0 ? 'Introduction' : tutorial.title}</span
>
</a>
{#if isCurrentStep && toc.length}
{#if isCurrentStep && absoluteToc.length}
<ol
class="web-references-menu-list u-margin-block-start-16 u-margin-inline-start-32"
>
{#each toc.slice(1) as parent}
{#each absoluteToc as parent, innerIndex}
<li class="web-references-menu-item">
<a
href={parent.href}
on:click|preventDefault={() =>
scrollToItem(parent, innerIndex)}
class="web-references-menu-link is-inner"
class:tutorial-scroll-indicator={parent.selected}
class:is-selected={parent.selected}
@@ -208,4 +269,49 @@
background: unset;
padding-inline-start: unset;
}
.u-margin-block-start-16 {
margin-block-start: 1rem;
}
.u-margin-inline-start-32 {
margin-inline-start: 2rem;
}
.web-references-menu-item:has(.is-selected)::before {
/* maintains the distance correctly for the children items */
inset-inline-start: -3.55rem;
}
/* Static slider: default slider for each selected link */
.web-references-menu-list > .web-references-menu-item > .is-selected::before {
content: ' ';
position: absolute;
inset-block-start: 0;
block-size: 1.375rem;
inline-size: 0.0625rem;
inset-inline-start: -1.3125rem;
background-color: hsl(var(--p-references-menu-link-color-text));
}
/* Hide static slider if any child menu item is selected */
.web-references-menu-list
> .web-references-menu-item:has(.web-references-menu-list .is-selected)
> .is-selected::before {
background-color: transparent;
}
/* Transparent slider for selected child items because we use parent level */
.web-references-menu-list
> .web-references-menu-item
> .web-references-menu-list
> .web-references-menu-item
> .is-selected::before {
content: '';
background-color: transparent;
}
:global(.tutorial-heading h2) {
margin-bottom: unset;
}
</style>

View File

@@ -1,11 +1,6 @@
<script lang="ts" context="module">
import { writable } from 'svelte/store';
export type NavLink = {
label: string;
href: string;
showBadge?: boolean;
};
export const isHeaderHidden = writable(false);
export const isMobileNavOpen = writable(false);
const initialized = writable(false);
@@ -21,12 +16,13 @@
import { addEventListener } from '@melt-ui/svelte/internal/helpers';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { classNames } from '$lib/utils/classnames';
import ProductsSubmenu from '$lib/components/ProductsSubmenu.svelte';
import ProductsMobileSubmenu from '$lib/components/ProductsMobileSubmenu.svelte';
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
import AnnouncementBanner from '$lib/components/AnnouncementBanner.svelte';
import InitBanner from '$lib/components/InitBanner.svelte';
import Button from '$lib/components/ui/Button.svelte';
import { trackEvent } from '$lib/actions/analytics';
import MainNav, { type NavLink } from '$lib/components/MainNav.svelte';
export let omitMainId = false;
let theme: 'light' | 'dark' | null = 'dark';
@@ -102,6 +98,11 @@
});
let navLinks: NavLink[] = [
{
label: 'Products',
submenu: ProductsSubmenu,
mobileSubmenu: ProductsMobileSubmenu
},
{
label: 'Docs',
href: '/docs'
@@ -240,23 +241,7 @@
width="130"
/>
</a>
<nav class="web-main-header-nav" aria-label="Main">
<ul class="web-main-header-nav-list">
{#each navLinks as navLink}
<li class="web-main-header-nav-item text-primary hover:text-accent">
<a
class={classNames(
'data-[badge]:after:animate-scale-in data-[badge]:relative data-[badge]:after:absolute data-[badge]:after:size-1.5 data-[badge]:after:translate-full data-[badge]:after:rounded-full'
)}
href={navLink.href}
data-initialized={$initialized ? '' : undefined}
data-badge={navLink.showBadge ? '' : undefined}
>{navLink.label}
</a>
</li>
{/each}
</ul>
</nav>
<MainNav initialized={$initialized} links={navLinks} />
</div>
<div class="web-main-header-end">
<a
@@ -303,25 +288,4 @@
.is-special-padding {
padding-inline: clamp(1.25rem, 4vw, 120rem);
}
[data-badge] {
position: relative;
&::after {
content: '';
position: absolute;
background-color: hsl(var(--web-color-accent));
border-radius: 100%;
width: 0.375rem;
height: 0.375rem;
inset-block-start: -2px;
inset-inline-end: -4px;
translate: 100%;
}
&:not([data-initialized])::after {
animation: scale-in 0.2s ease-out;
}
}
</style>

View File

@@ -6,7 +6,7 @@
</script>
<a
class="web-side-nav-button"
class="web-side-nav-button flex size-10 w-full items-center whitespace-nowrap rounded-lg p-2"
class:is-selected={$page.url?.pathname === groupItem.href}
href={groupItem.href}
target={groupItem.openInNewTab ? '_blank' : '_self'}

View File

@@ -57,6 +57,15 @@ export enum Platform {
ServerRest = 'server-rest'
}
export enum Framework {
NextJs = 'Next.js',
SvelteKit = 'SvelteKit',
VueJs = 'Vue.js',
Nuxt3 = 'Nuxt3',
Astro = 'Astro',
Remix = 'Remix'
}
export const platformMap: Record<Language | string, string> = {
[Platform.ClientApple]: 'Apple',
[Platform.ClientFlutter]: 'Flutter',

View File

@@ -199,28 +199,19 @@ export function getSchema(id: string, api: OpenAPIV3.Document): OpenAPIV3.Schema
}
const specs = import.meta.glob(
'$appwrite/app/config/specs/open-api3*-(client|server|console).json',
{
query: '?raw',
import: 'default'
}
'$appwrite/app/config/specs/open-api3*-(client|server|console).json'
);
async function getSpec(version: string, platform: string) {
export async function getApi(version: string, platform: string): Promise<OpenAPIV3.Document> {
const isClient = platform.startsWith('client-');
const isServer = platform.startsWith('server-');
const target = `/node_modules/@appwrite.io/repo/app/config/specs/open-api3-${version}-${
isServer ? 'server' : isClient ? 'client' : 'console'
}.json`;
return specs[target]();
}
export async function getApi(version: string, platform: string): Promise<OpenAPIV3.Document> {
const raw = await getSpec(version, platform);
const api = JSON.parse(raw);
return api;
}
const descriptions = import.meta.glob(
'/src/routes/docs/references/[version]/[platform]/[service]/descriptions/*.md',
{
@@ -236,9 +227,7 @@ export async function getDescription(service: string): Promise<string> {
throw new Error('Missing service description');
}
const description = descriptions[target]();
return description;
return descriptions[target]();
}
export async function getService(
@@ -327,9 +316,11 @@ export async function getService(
continue;
}
const demo = await examples[path]();
data.methods.push({
id: operation['x-appwrite'].method,
demo: await examples[path](),
demo: demo ?? '',
title: operation.summary ?? '',
description: operation.description ?? '',
parameters: parameters ?? [],
@@ -370,9 +361,9 @@ export function resolveReference(
export const generateExample = (
schema: OpenAPIV3.SchemaObject,
api: OpenAPIV3.Document<{}>,
api: OpenAPIV3.Document<object>,
modelType: ModelType = ModelType.REST
): Object => {
): object => {
const properties = Object.keys(schema.properties ?? {}).map((key) => {
const name = key;
const fields = schema.properties?.[key];

View File

@@ -3,7 +3,7 @@ import type { Tutorial } from '$markdoc/layouts/Tutorial.svelte';
export function globToTutorial(data: { tutorials: Record<string, unknown>; pathname: string }) {
let isFound = false;
let difficulty, readtime;
let difficulty: string | undefined, readtime: string | undefined;
return Object.entries(data.tutorials)
.map(([filepath, tutorial]) => {

17
src/lib/utils/utm.ts Normal file
View File

@@ -0,0 +1,17 @@
export function getReferrerAndUtmSource() {
if (sessionStorage) {
let values = {};
if (sessionStorage.getItem('utmReferral')) {
values = { ...values, utmReferral: sessionStorage.getItem('utmReferral') };
}
if (sessionStorage.getItem('utmSource')) {
values = { ...values, utmSource: sessionStorage.getItem('utmSource') };
}
if (sessionStorage.getItem('utmMedium')) {
values = { ...values, utmMedium: sessionStorage.getItem('utmMedium') };
}
return values;
}
return {};
}

View File

@@ -45,11 +45,11 @@
<span>Back to blog</span>
</a>
<div class="web-category-header mt-6">
<div class="web-category-header-content">
<div class="flex flex-col justify-between gap-6 md:flex-row md:items-center">
<h1 class="text-display font-aeonik-pro text-primary">
{name}
</h1>
<p class="web-category-header-description text-description">
<p class="text-secondary text-description">
{description}
</p>
</div>

View File

@@ -2,7 +2,6 @@
import { Root, Slide } from '$lib/components/carousel';
import FooterNav from '$lib/components/FooterNav.svelte';
import MainFooter from '$lib/components/MainFooter.svelte';
import ProductsGrid from '$lib/components/ProductsGrid.svelte';
import { Main } from '$lib/layouts';
import { DEFAULT_HOST } from '$lib/utils/metadata';
import type { Integration } from '$routes/integrations/+page';
@@ -44,7 +43,7 @@
class="web-u-sep-block-end pb-0"
style="background-color:rgba(23, 23, 26, 1); margin-block-end: 2.5rem"
>
<div class="container">
<div class="container dark">
<div class="web-integrations-top-section">
<div class="web-carousel-wrapper">
<a href="/integrations" class="web-button is-text mb-12">
@@ -181,13 +180,8 @@
</Main>
<style lang="scss">
@use '$scss/abstract' as *;
.cta {
min-height: pxToRem(560);
display: flex;
align-items: center;
}
@use '$scss/abstract/functions' as f;
@use '$scss/abstract/variables/devices';
.web-pre-footer-bg {
position: absolute;
@@ -199,24 +193,15 @@
max-inline-size: unset;
max-block-size: unset;
}
/* more tha 9 items */
.l-side-column {
display: flex;
gap: pxToRem(16);
@media #{$break1} {
flex-direction: column;
}
}
.l-grid-2-1 {
@media #{$break1} {
@media #{devices.$break1} {
display: flex;
flex-direction: column;
}
@media #{$break2open} {
@media #{devices.$break2open} {
display: grid;
gap: pxToRem(64);
gap: f.pxToRem(64);
grid-template-columns: repeat(12, 1fr);
}

View File

@@ -72,23 +72,14 @@
<header class="web-grid-120-1fr-auto-header">
<h1 class="text-title font-aeonik-pro text-primary">{title}</h1>
</header>
<button
class="toc-btn web-u-padding-20 web-u-margin-inline-20-negative text-primary web-is-only-mobile web-u-sep-block
web-u-filter-blur-8 sticky mt-6 flex
w-full items-center justify-between"
style:--inset-block-start="4.5rem"
style:inline-size="100vw"
style:background-color="hsl(var(--p-body-bg-color) / 0.1)"
style:translate="0 {$isHeaderHidden ? '-4.5rem' : '0'}"
style:z-index="1"
on:click={() => (showToc = !showToc)}
>
<span class="text-description">Table of contents</span>
<span class="icon-menu-alt-4" aria-hidden="true" />
</button>
<TocNav />
<TocNav bind:showToc />
<main class="web-grid-120-1fr-auto-main /web-is-mobile-closed" id="main">
<div class="web-content is-count-headers" class:web-is-mobile-closed={showToc}>
<div
class="web-content is-count-headers"
class:web-is-mobile-closed={showToc && !showToc}
>
<!-- svelte-ignore a11y-hidden -->
<h2 aria-hidden="true">Introduction</h2>
<slot />
@@ -107,7 +98,20 @@
opacity: 0;
}
.toc-btn {
transition: translate 0.3s ease;
@media (max-width: 768px) {
.container {
padding-left: 0;
padding-right: 0;
}
header {
padding-block-end: unset;
}
header,
main {
padding-left: var(--spacing-5, 1.25rem);
padding-right: var(--spacing-5, 1.25rem);
}
}
</style>

View File

@@ -236,7 +236,7 @@
</div>
</div>
<div class="web-u-sep-block-start py-10">
<div class="web-u-sep-block-start pt-10">
<div class="web-big-padding-section-level-2">
<div class="container">
<h3 class="text-label text-primary">Read next</h3>

View File

@@ -7,6 +7,7 @@
import { copy } from '$lib/utils/copy';
import type { CodeContext } from '../tags/MultiCode.svelte';
import { melt } from '@melt-ui/svelte';
import { isInTutorialDocs } from '$lib/layouts/Docs.svelte';
export let content: string;
export let toCopy: string | undefined = undefined;
@@ -15,6 +16,7 @@
export let withLineNumbers = true;
export let badge: string | null = null;
const inTutorialDocs = isInTutorialDocs();
const insideMultiCode = hasContext('multi-code');
const selected = insideMultiCode ? getContext<CodeContext>('multi-code').selected : null;
@@ -61,7 +63,11 @@
{@html result}
{/if}
{:else}
<section class="dark web-code-snippet" aria-label="code-snippet panel">
<section
class="dark web-code-snippet"
class:no-top-margin={inTutorialDocs}
aria-label="code-snippet panel"
>
<header class="web-code-snippet-header">
<div class="web-code-snippet-header-start">
{#if badgeValue}
@@ -98,3 +104,9 @@
</div>
</section>
{/if}
<style>
.no-top-margin {
margin-top: unset !important;
}
</style>

View File

@@ -6,7 +6,13 @@
const isExternal = ['http://', 'https://'].some((prefix) => href.startsWith(prefix));
const target = isExternal ? '_blank' : undefined;
const rel = isExternal ? 'noopener nofollow' : undefined;
const doFollow = href.includes('?dofollow=true') || href.includes('&dofollow=true');
if (doFollow) {
href = href.replace(/[?&]dofollow=true/g, '').replace(/[?&]$/, '');
}
const rel = isExternal ? `noopener${doFollow ? '' : ' nofollow'}` : undefined;
const inChangelog = isInChangelog();

View File

@@ -3,10 +3,11 @@
import { isInPolicy } from '$markdoc/layouts/Policy.svelte';
import { getContext, hasContext } from 'svelte';
import { isInTable } from './Table.svelte';
import { isInDocs } from '$lib/layouts/Docs.svelte';
import { isInDocs, isInTutorialDocs } from '$lib/layouts/Docs.svelte';
const noParagraph = hasContext('no-paragraph') ? getContext('no-paragraph') : false;
const inDocs = isInDocs();
const inTutorialDocs = isInTutorialDocs();
const inPolicy = isInPolicy();
const inChangelog = isInChangelog();
const inTable = isInTable();
@@ -18,6 +19,7 @@
if (inDocs) return 'text-paragraph-md mb-8';
if (inPolicy) return 'text-paragraph-md mb-4';
if (inTable) return 'text-paragraph-md';
if (inTutorialDocs) return 'text-paragraph-md mb-2';
if (inChangelog) return 'text-paragraph-lg mb-4 font-normal';
return 'text-paragraph-lg mb-8';
})();

View File

@@ -5,12 +5,6 @@
export let url: string = PUBLIC_APPWRITE_DASHBOARD;
</script>
<div class="call-to-action">
<div class="py-12">
<a href={url} class="web-button">{label}</a>
</div>
<style lang="scss">
.call-to-action {
margin: 48px 0;
}
</style>

View File

@@ -1,9 +1,15 @@
<script lang="ts">
import { type Writable } from 'svelte/store';
import Heading from '../nodes/Heading.svelte';
import { getContext, hasContext } from 'svelte';
export let id: string;
export let step: number;
export let title: string;
if (hasContext('articleHasNumericBadge')) {
getContext<Writable<boolean>>('articleHasNumericBadge').set(true);
}
</script>
<section class="web-article-content-section is-with-line">

View File

@@ -7,6 +7,8 @@
</script>
<script lang="ts">
import Select from '$lib/components/Select.svelte';
import { classNames } from '$lib/utils/classnames';
import { createTabs } from '@melt-ui/svelte';
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
@@ -27,20 +29,82 @@
<div class="web-card is-normal mt-4" {...$root} use:root>
<div
class="tabs flex gap-4 overflow-scroll"
class="tabs flex items-center gap-4 overflow-scroll"
style="scrollbar-width: none; -ms-overflow-style: none;"
>
<ul class="tabs-list flex items-center gap-4" {...$list} use:list>
{#each $ctx.triggers.entries() as [id, title]}
<li class="tabs-item rounded-t-[0.625rem] hover:bg-white/4">
<ul class="tabs-list hidden items-center gap-4 sm:flex" {...$list} use:list>
{#each Array.from($ctx.triggers.entries()).slice(0, 7) as [id, title]}
<li
class="tabs-item rounded-t-[0.625rem] text-center hover:bg-white/4"
class:text-[var(--color-primary)]={$value === id}
>
<button
class="tabs-button cursor-pointer bg-clip-padding py-[0.625rem] px-1 font-light outline-none"
class:is-selected={$value === id}
class={classNames(
'tabs-button relative cursor-pointer bg-clip-padding py-[0.625rem] px-1 font-light outline-none',
'after:relative after:top-1 after:bottom-0 after:block after:h-px after:transition-all',
{
'after:bg-[var(--color-primary)]': $value === id
}
)}
{...$trigger(id)}
use:trigger>{title}</button
>
</li>
{/each}
{#if Array.from($ctx.triggers.entries()).slice(7, Array.from($ctx.triggers.entries()).length - 1).length}
{@const entries = Array.from($ctx.triggers.entries())}
{@const desktopOptions = entries.slice(7, entries.length - 1)}
<li>
<Select
initialLabel="More"
options={desktopOptions.map(([value, label]) => {
return {
value,
label
};
})}
bind:value={$value}
/>
</li>
{/if}
</ul>
<ul class="tabs-list flex items-center gap-4 sm:hidden" {...$list} use:list>
{#each Array.from($ctx.triggers.entries()).slice(0, 3) as [id, title]}
<li
class="tabs-item rounded-t-[0.625rem] text-center hover:bg-white/4"
class:text-[var(--color-primary)]={$value === id}
>
<button
class={classNames(
'tabs-button relative cursor-pointer bg-clip-padding py-[0.625rem] px-1 font-light outline-none',
'after:relative after:top-1 after:bottom-0 after:block after:h-px after:transition-all',
{
'after:bg-[var(--color-primary)]': $value === id
}
)}
{...$trigger(id)}
use:trigger>{title}</button
>
</li>
{/each}
{#if Array.from($ctx.triggers.entries()).slice(3, Array.from($ctx.triggers.entries()).length - 1).length}
{@const entries = Array.from($ctx.triggers.entries())}
{@const desktopOptions = entries.slice(3, entries.length - 1)}
<li>
<Select
initialLabel="More"
options={desktopOptions.map(([value, label]) => {
return {
value,
label
};
})}
bind:value={$value}
/>
</li>
{/if}
</ul>
</div>
<slot />

View File

@@ -1,3 +1,31 @@
# Persistence {% #persistence %}
Appwrite handles the persistence of the session in a consistent way across SDKs. After authenticating with an SDK, the SDK will persist the session so that the user will not need to log in again the next time they open the app. The mechanism for persistence depends on the SDK.
{% info title="Best Practice" %}
Only keep user sessions active as long as needed and maintain exactly **one** instance of the Client SDK in your app to avoid conflicting session data.
{% /info %}
| {% width=70 %} | Framework {% width=120 %} | Storage method |
| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------: | :--------------------------------------------------------------------------------------------------: |
| {% only_dark %}{% icon_image src="/images/platforms/dark/javascript.svg" alt="Javascript logo" size="m" /%}{% /only_dark %}{% only_light %}{% icon_image src="/images/platforms/javascript.svg" alt="Javascript logo" size="m" /%}{% /only_light %} | Web | Uses a secure session cookie and falls back to local storage when a session cookie is not available. |
| {% only_dark %}{% icon_image src="/images/platforms/dark/flutter.svg" alt="Javascript logo" size="m" /%}{% /only_dark %}{% only_light %}{% icon_image src="/images/platforms/flutter.svg" alt="Javascript logo" size="m" /%}{% /only_light %} | Flutter | Uses a session cookie stored in Application Documents through the **path_provider** package. |
| {% only_dark %}{% icon_image src="/images/platforms/dark/apple.svg" alt="Javascript logo" size="m" /%}{% /only_dark %}{% only_light %}{% icon_image src="/images/platforms/apple.svg" alt="Javascript logo" size="m" /%}{% /only_light %} | Apple | Uses a session cookie stored in **UserDefaults**. |
| {% only_dark %}{% icon_image src="/images/platforms/dark/android.svg" alt="Javascript logo" size="m" /%}{% /only_dark %}{% only_light %}{% icon_image src="/images/platforms/android.svg" alt="Javascript logo" size="m" /%}{% /only_light %} | Android | Uses a session cookie stored in **SharedPreferences**. |
# Session limits {% #session-limits %}
In Appwrite versions 1.2 and above, you can limit the number of active sessions created per user to prevent the accumulation of unused but active sessions. New sessions created by the same user past the session limit delete the oldest session.
You can change the session limit in the **Security** tab of the Auth Service in your Appwrite Console. The default session limit is 10 with a maximum configurable limit of 100.
# Permissions {% #permissions %}
Security is very important to protect users' data and privacy.
Appwrite uses a [permissions model](/docs/advanced/platform/permissions) coupled with user sessions to ensure users need correct permissions to access resources.
With all Appwrite services, including databases and storage, access is granted at the collection, bucket, document, or file level.
These permissions are enforced for client SDKs and server SDKs when using JWT, but are ignored when using a server SDK with an API key.
# Password history {% #password-history %}
Password history prevents users from reusing recent passwords. This protects user accounts from security risks by enforcing a new password every time it's changed.
@@ -34,3 +62,19 @@ Enable email alerts for your users so that whenever another session is created f
You won't receive notifications when logging in using [Magic URL](/docs/products/auth/magic-url), [Email OTP](/docs/products/auth/email-otp), or [OAuth2](/docs/products/auth/oauth2) since these authentication methods already verify user access to their systems, establishing the authentication's legitimacy.
To toggle session alerts, navigate to **Auth** > **Security** > **Session alerts**.
# Memberships privacy {% #memberships-privacy %}
In certain use cases, your app may not need to share members' personal information with others. You can safeguard privacy by marking specific membership details as private. To configure this setting, navigate to **Auth** > **Security** > **Memberships privacy**
These details can be made private:
- `userName` - The member's name
- `userEmail` - The member's email address
- `mfa` - Whether the member has enabled multi-factor authentication
# Mock phone numbers {% #mock-phone-numbers %}
Creating and using mock phone numbers allows users to test SMS authentication without needing an actual phone number. This can be useful for testing edge cases where a user doesn't have a phone number but needs to sign in to your application using SMS.
To create a mock phone number, navigate to **Auth** > **Security** > Mock Phone Numbers. After defining a mock phone number, you need to define a specific OTP code that will be used for SMS sign-in instead of the SMS secret code sent to a real phone number.

View File

@@ -0,0 +1,18 @@
# Prohibited activities {% #prohibited-activities %}
The following actions are prohibited on the Appwrite platform and may lead to immediate suspension or termination:
- **Illegal and harmful content:** Sharing any content that is illegal, infringing (e.g., copyright infringement), harmful, threatening, defamatory, obscene, harassing, or otherwise objectionable. This includes distributing malware, viruses, or any malicious code.
- **Unauthorized access and disruption:** Accessing or attempting to access any system, data, or account without authorization. This includes:
- Hacking
- Penetration testing without approval
- Denial-of-Service (DoS) attacks
- Disrupting Appwrite Cloud's integrity or performance (e.g., excessive resource usage, unauthorized load testing)
- **Deceptive practices:** Engaging in any fraudulent or deceptive activity, such as:
- Phishing
- Misleading others
- Circumventing payment obligations
- **Unsolicited communications:** Sending spam, unauthorized advertising, or any form of improper solicitation.
- **Misuse of resources:**
- Using Appwrite Cloud for cryptocurrency mining without authorization
- Violating any applicable laws or regulations

View File

@@ -674,5 +674,41 @@
{
"link": "/cli/install.sh",
"redirect": "https://raw.githubusercontent.com/appwrite/sdk-for-cli/master/install.sh"
},
{
"link": "/case-studies",
"redirect": "/customer-stories"
},
{
"link": "/blog/category/case-studies",
"redirect": "/blog/category/customer-stories"
},
{
"link": "/blog/post/case-study-smartbee",
"redirect": "/blog/post/customer-stories-smartbee"
},
{
"link": "/blog/post/case-study-kcollect",
"redirect": "/blog/post/customer-stories-kcollect"
},
{
"link": "/blog/post/case-study-majik-kids",
"redirect": "/blog/post/customer-stories-majik-kids"
},
{
"link": "/blog/post/case-study-myshoefitter",
"redirect": "/blog/post/customer-stories-myshoefitter"
},
{
"link": "/blog/post/case-study-open-mind",
"redirect": "/blog/post/customer-stories-open-mind"
},
{
"link": "/blog/post/case-study-langx",
"redirect": "/blog/post/customer-stories-langx"
},
{
"link": "/blog/post/case-study-undo",
"redirect": "/blog/post/customer-stories-undo"
}
]

View File

@@ -74,6 +74,15 @@
if (ref || referrer || utmSource || utmCampaign || utmMedium) {
createSource(ref, referrer, utmSource, utmCampaign, utmMedium);
}
if (referrer || ref) {
sessionStorage.setItem('utmReferral', referrer ? referrer : (ref ?? ''));
}
if (utmSource) {
sessionStorage.setItem('utmSource', utmSource);
}
if (utmMedium) {
sessionStorage.setItem('utmMedium', utmMedium);
}
const initialTheme = $page.route.id?.startsWith('/docs') ? getPreferredTheme() : 'dark';
applyTheme(initialTheme);

View File

@@ -11,11 +11,12 @@
import MainFooter from '../lib/components/MainFooter.svelte';
import DeveloperCard from './DeveloperCard.svelte';
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
import CoverImage from './dashboard.png';
import CoverImage from './dashboard.webp';
import Hero from '$lib/components/ui/Hero.svelte';
import GradientText from '$lib/components/ui/GradientText.svelte';
import Badge from '$lib/components/ui/Badge.svelte';
import { trackEvent } from '$lib/actions/analytics';
import AppwriteIn100Seconds from '$lib/components/AppwriteIn100Seconds.svelte';
const title = 'Appwrite - Build like a team of hundreds';
const description = DEFAULT_DESCRIPTION;
@@ -92,7 +93,7 @@
<enhanced:img
style="width:1466px; height:804px; transform:rotate(150.348deg); opacity: 0.65; filter: blur(127.5px);
max-block-size: unset; max-inline-size: unset;"
src="./top-page-dark.png"
src="./top-page-dark.webp"
alt=""
/>
</div>
@@ -103,7 +104,7 @@
style="top: 22rem; left: 54%; translate: calc(-50% - 900px); width: 75.9375rem;"
class:web-u-hide-mobile={$isMobileNavOpen}
>
<img src="/images/bgs/hero-lines-1.png" alt="" />
<img src="/images/bgs/hero-lines-1.webp" alt="" />
</div>
<div
@@ -112,7 +113,7 @@
class:web-u-hide-mobile={$isMobileNavOpen}
>
<div style="left: 0;">
<img src="/images/bgs/hero-lines-2.png" alt="" />
<img src="/images/bgs/hero-lines-2.webp" alt="" />
</div>
</div>
@@ -129,9 +130,7 @@
<span class="web-icon-star shrink-0" aria-hidden="true" />
<span class="text-caption shrink-0 font-medium">New</span>
<div class="web-hero-banner-button-sep" />
<span class="text-caption web-u-trim-1"
>Introducing Database Backups</span
>
<span class="text-caption web-u-trim-1">Introducing Database Backups</span>
<span class="web-icon-arrow-right shrink-0" aria-hidden="true" />
</a>
@@ -147,14 +146,17 @@
Functions, Storage, and Messaging to your projects using the frameworks
and languages of your choice.
</svelte:fragment>
<a
href={PUBLIC_APPWRITE_DASHBOARD}
class="web-button mt-8 w-full lg:w-fit"
slot="cta"
on:click={() => trackEvent('Get started in hero')}
>
Get started
</a>
<div class="mt-8 flex flex-col gap-4 sm:flex-row" slot="cta">
<a
href={PUBLIC_APPWRITE_DASHBOARD}
class="web-button w-full lg:w-fit"
on:click={() => trackEvent('Get started in hero')}
>
Get started
</a>
<AppwriteIn100Seconds />
</div>
</Hero>
</section>
</div>

View File

@@ -35,6 +35,8 @@
selectedMap = selectedMap;
};
};
let showToc = false;
</script>
<svelte:head>
@@ -64,19 +66,11 @@
<div class="web-grid-120-1fr-auto">
<header class="web-grid-120-1fr-auto-header">
<h1 class="text-display font-aeonik-pro text-primary">Brand assets</h1>
<button
class="web-u-padding-block-20 text-primary web-is-only-mobile
web-u-margin-inline-32-negative web-u-sep-block
mt-6 flex w-full w-full"
>
<span class="container flex w-full items-center justify-between">
<span class="text-description">Table of contents</span>
<span class="icon-menu-alt-4" aria-hidden="true" />
</span>
</button>
</header>
<TocNav />
<main class="web-grid-120-1fr-auto-main /web-is-mobile-closed" id="main">
<TocNav bind:showToc />
<main class="web-grid-120-1fr-auto-main" id="main">
<div class="web-content">
<section>
<p>
@@ -628,4 +622,21 @@
}
}
}
@media (max-width: 768px) {
.container {
padding-left: 0;
padding-right: 0;
}
header {
padding-block-end: unset;
}
header,
main {
padding-left: var(--spacing-5, 1.25rem);
padding-right: var(--spacing-5, 1.25rem);
}
}
</style>

View File

@@ -1,6 +0,0 @@
---
layout: category
name: Case Studies
description: Learn more about the product development and growth journeys of our best customers.
---

View File

@@ -0,0 +1,6 @@
---
layout: category
name: Customer Stories
description: Learn more about how other developers have built successful applications while relying on Appwrite.
---

View File

@@ -0,0 +1,197 @@
---
layout: post
title: 10 new Git commands you should start using today
description: Learn these Git commands to make your workflow smoother, faster, and flexible.
date: 2024-12-12
cover: /images/blog/10-git-commands-you-should-start-using/cover.png
timeToRead: 10
author: ebenezer-don
category: tutorial
featured: false
callToAction: true
---
If you've worked with Git long enough, you've probably hit some common frustrations like operations getting slower as repositories grow, accidentally overwriting changes when switching branches, or struggling with massive monorepos.
Thankfully, just like every other tool, Git is constantly evolving and adding new features to make our lives easier. While some of these commands aren't particularly recent, they remain lesser-known gems that can significantly improve your workflow. If you're already familiar with some of the core tips and tricks—like those covered in [15 Git command line tips every developer should know](https://appwrite.io/blog/post/15-git-cli-tips?dofollow=true), this article will introduce you to ten additional commands that can take your Git skills to the next level.
# 1. git switch - A safer way to change branches
Before Git 2.23, `git checkout` was the main command for switching branches, but it did much more than that. You could use it to restore files, create branches, or check out specific commits. This made it powerful but potentially confusing - especially when you just wanted to switch branches without touching your files.
That's why Git 2.23 introduced `git switch` as a more focused alternative for branch operations. With `git switch`, you can focus solely on branch management:
```bash
# Move to another branch
git switch feature-branch
# Create and switch to a new branch
git switch -c new-branch
```
This clarity reduces the risk of accidentally overwriting files or making unintended changes. If you've ever hesitated to use `git checkout` for fear of doing something wrong, `git switch` simplifies the process.
# 2. git restore - Safely undo changes
Undoing changes often involved using `git checkout` to revert files or `git reset` to move the branch HEAD. However, both commands had the potential to alter your branch state if used incorrectly: `git reset` could move your branch HEAD, while `git checkout` could switch branches or check out a different commit, disrupting the current branch.
Git 2.23 introduced `git restore` to focus solely on undoing changes to files. It provides a safer and more straightforward way to revert changes in your working directory or staging area, clearly separating file operations from branch management tasks:
```bash
# Discard working directory changes
git restore main.js
# Unstage changes from the index
git restore --staged main.js
```
This is especially useful for beginners or in high-stakes situations where precision matters. You can undo changes without worrying about accidentally switching branches or resetting commits.
# 3. git maintenance - Automate repository health
As repositories grow, performance can degrade. Operations like `git fetch`, `git status`, or `git log` may slow down, and unused data can clutter your repository. Before Git 2.29, you'd have to manually run commands like `git gc` (garbage collection) or `git repack` to keep your repository optimized.
Git 2.29 introduced `git maintenance`, which automates these tasks for you:
```bash
# Enable automatic maintenance
git maintenance start
# Run cleanup tasks immediately
git maintenance run
```
**What's happening behind the scenes?**
- **Garbage Collection:** Removes unreachable objects, such as commits discarded during rebases or branch deletions.
- **Repacking:** Consolidates fragmented packfiles for better storage efficiency.
- **Commit Graph Updates:** Optimizes commit history traversal, speeding up commands like `git log` and `git blame`.
Using `git maintenance` will help you keep your repository healthy without manual effort.
# 4. git sparse-checkout - Efficiently handle large repositories
Monorepos are great for managing multiple projects, but cloning an entire repository when you only need a specific directory can be inefficient. Git 2.25 introduced `git sparse-checkout` to solve this.
```bash
# Enable sparse-checkout mode
git sparse-checkout init
# Fetch only specific directories
# You can specify multiple directories separated by spaces
git sparse-checkout set services/ docs/
```
With `git sparse-checkout`, you can include only the directories or files you need in your working directory, leaving the rest untouched. This is useful for large teams working on distinct parts of a monorepo, and will save you time and disk space.
# 5. git log --remerge-diff: Understand merges better
Merge commits often show which branches were merged, but they don't always explain the specific changes introduced, especially when conflicts were resolved during the merge.
Starting with Git 2.35, you can use:
```bash
git log --remerge-diff
```
This option reconstructs the merge commit by replaying the recorded merge strategy and showing the exact changes it introduced. It's useful for debugging merge conflicts or reviewing a complicated merge history.
# 6. git blame --ignore-rev - Ignore noisy commits
When your team makes a bulk formatting change, `git blame` can lose its utility, as every line ends up pointing to the formatting commit instead of the original author.
Introduced in Git 2.23, the `--ignore-rev` option allows you to exclude such commits:
```bash
git blame --ignore-rev commit-hash
```
To persist this exclusion, you can set up an ignore-revs file:
```bash
# Add the commit hash to the ignore-revs fileecho commit-hash >> .git-blame-ignore-revs
# Tell Git to use the file
git config blame.ignoreRevsFile .git-blame-ignore-revs
```
This helps you focus on meaningful authorship and can be useful in codebases with frequent style updates.
# 7. git range-diff - Compare and track changes between commit ranges
Rewriting history, whether through rebasing, cherry-picking, or interactive editing, can be tricky. After a rebase, you might wonder how the rewritten commits differ from the originals. `git range-diff` helps by comparing two commit ranges, showing how one evolved into the other and highlighting changes to individual commits:
```bash
git range-diff
```
This command can be used to understand the evolution of a feature or bug fix across different branches.
# 8. git worktree - Work on multiple branches simultaneously
Switching branches in a single working directory can disrupt your workflow, especially when you need to work across multiple branches. With `git worktree`, you can create additional working directories tied to the same repository.
```bash
# Add a new worktree for a specific branch
git worktree add ../feature-branch feature-branch
# Remove a worktree when you're done
git worktree remove ../feature-branch
```
`git worktree` allows you to work on different branches without switching or stashing. You can also create throwaway worktrees with detached HEADs for testing, or isolate builds and deployments in separate working directories.
# 9. git rebase --update-refs - Keep references in sync
Rebasing rewrites history by replacing old commits with new ones, but this often leaves branch pointers or tags referencing outdated commits. Git 2.38 introduces the `--update-refs` option to handle this automatically:
```bash
git rebase --update-refs
```
With this command, Git ensures that related branches and tags referencing rewritten commits are updated to match the new history. This eliminates the need for tedious manual updates and ensures consistency across your repository.
For even more control, you can configure git rebase to always update specific refs by setting:
```bash
git config rebase.updateRefs true
```
This is useful in collaborative workflows or when managing multiple refs tied to the same history.
# 10. git commit --fixup and git rebase --autosquash - Fixup commits
While not a new feature (introduced in Git 1.7.4, back in 2011), `git commit --fixup` is often overlooked despite being a useful tool for maintaining clean commit histories. When working on a feature, you might realize that you need to fix or improve a previous commit. Manually editing your commit history to include these changes can lead to errors. Git provides `git commit --fixup` and `git rebase --autosquash` to automate this process.
```bash
# Create a fixup commit targeting a specific commit
git commit --fixup=<commit-hash>
# Later, during an interactive rebase, automatically squash fixup commits
git rebase -i --autosquash <base-branch>
```
The `--fixup` option creates a commit that's marked to be automatically squashed into the target commit during an interactive rebase with `--autosquash`. This streamlines the process of cleaning up your commit history before merging changes, and will ensure that related changes are grouped together without manual effort.
# Conclusion
The commands we've discussed in this article can help you solve real problems you might be facing every day as a Git user. Whether you're managing a monorepo, handling large histories, or trying to keep your repository clean, these practical solutions can make a difference. Start with one or two that fit your current workflow, and you might be surprised by the improvements in your productivity.
If you liked this article, you might also enjoy [15 Git command line tips every developer should know](https://appwrite.io/blog/post/15-git-cli-tips?dofollow=true).
# More resources
- [How to implement Sign in with GitHub](https://appwrite.io/blog/post/implement-sign-in-with-github)
- [SQL vs NoSQL: Choosing the right database for your project](https://appwrite.io/blog/post/sql-vs-nosql)
- [Deno 2 vs Bun: which JavaScript runtime is right for you?](https://appwrite.io/blog/post/deno-vs-bun-javascript-runtime)

View File

@@ -8,11 +8,12 @@ timeToRead: 6
author: ebenezer-don
category: tutorial
featured: false
callToAction: true
---
While the command line interface can seem intimidating on the surface, it's actually a very useful tool that gives you control over your code in ways that GUIs often don't. If you can get comfortable with even a few git commands, you'll find yourself being more productive.
In this guide, we'll cover 15 Git command line tips that will help make your workflow smoother, faster, and flexible, whether you're working solo or with a team.
In this guide, we'll cover 15 essential git commands you should know as a developer, however, if you're looking for more advanced commands, you can check out these [10 new Git commands you should start using today](https://appwrite.io/blog/post/10-git-commands-you-should-start-using?dofollow=true).
# 1. git init - start a new repository
@@ -214,12 +215,13 @@ If something goes wrong, `reflog` can provide a trail to recover lost changes, m
# Conclusion
These 15 Git command line tips lay a strong foundation for both solo projects and team-based workflows. Mastering these basics ensures that you'll work efficiently, minimize errors, and maintain a clean project history. Over time, as you get comfortable with each command, your confidence in managing code will grow.
These 15 Git command line tips lay a strong foundation for both solo projects and team-based workflows. Mastering these basics ensures that you'll work efficiently, minimize errors, and maintain a clean project history.
Over time, as you get comfortable with each command, your confidence in managing code will grow, and you'll be able to easily add these [10 new Git commands](https://appwrite.io/blog/post/10-git-commands-you-should-start-using?dofollow=true) to your workflow.
Git's power is in its flexibility, and learning the command line lets you tap into that fully. Practice them, apply them, and let them become second nature for a smoother development experience.
# More resources
- [How to implement Sign in with GitHub](https://appwrite.io/blog/post/implement-sign-in-with-github)
- [Building a currency converter API with Deno 2 and Appwrite](https://appwrite.io/blog/post/build-a-currency-converter-with-deno)
- [Local serverless function development with the new Appwrite CLI](https://appwrite.io/blog/post/functions-local-development-guide)
- [10 new Git commands you should start using today](https://appwrite.io/blog/post/10-git-commands-you-should-start-using?dofollow=true)
- [Building a currency converter API with Deno 2 and Appwrite](https://appwrite.io/blog/post/build-a-currency-converter-with-deno)

View File

@@ -35,7 +35,7 @@ Pro gives you much more room and flexibility to build, scale, and maintain your
- Unlimited projects (never paused)
- Unlimited projects (never paused)
---
- 10GB bandwidth
- 5GB bandwidth
- 300GB bandwidth
---
- 2GB storage

View File

@@ -8,6 +8,7 @@ timeToRead: 7
author: aditya-oberai
category: product
unlisted: true
callToAction: true
---
If you are looking to build a mobile app, website, tool, or any other application that needs a backend, then you also know the daunting tasks that await. This is probably what brought you to this blog in the first place: looking for a solution to take care of your backend. One of these solutions is a Backend-as-a-Service (BaaS). It provides pre-built backend infrastructure and services to simplify app development, handling server-side tasks like data storage, user management, APIs, server maintenance, security, database management, and more. Two of these solutions are Appwrite and Supabase, and although both are solid options for your BaaS, theyre somewhat different.

View File

@@ -65,7 +65,7 @@ Cloudinary is a cloud service that focuses on managing media assets, especially
| **FEATURE** | **STORAGE** | **CLOUDINARY** |
| --- | --- | --- |
| Deployment | Self-hosted or cloud-hosted | Cloud |
| Free plan | 2GB storage, 10GB bandwidth | 25GB storage/bandwidth |
| Free plan | 2GB storage, 5GB bandwidth | 25GB storage/bandwidth |
| Paid plan | $15 per member/month for increased bandwidth, users and storage | $89 to $224 per month for increased users and credits |
| Open source | Yes ✅ | No ❌ |
| Support | Discord and email, dedicated channels for startups | Community and email, paid support options |

View File

@@ -8,8 +8,9 @@ timeToRead: 8
author: aditya-oberai
category: product
featured: false
callToAction: true
---
Serverless functions are a powerful tool for developers designed to provide flexibility and simplify backend tasks. With serverless functions, you can focus more on writing code and less on managing infrastructure, making your work faster and more efficient.
Serverless functions are a powerful tool for developers designed to provide flexibility and simplify backend tasks. With serverless functions, you can focus more on writing code and less on managing infrastructure, making your work faster and more efficient.
In this comparison, we'll take a look at the serverless functions offered by three popular backend-as-a-service platforms: Firebase, Supabase, and Appwrite.
@@ -38,7 +39,7 @@ Firebase Cloud Functions allow you to run backend code in response to events tri
![Supabase-functions](/images/blog/comparing-functions/2.png)
## Overview:
Supabase, an open-source Firebase alternative, is rapidly gaining popularity for its seamless integration with PostgreSQL. It provides developers with a powerful SQL database, real-time capabilities, and serverless functions.
Supabase, an open-source Firebase alternative, is rapidly gaining popularity for its seamless integration with PostgreSQL. It provides developers with a powerful SQL database, real-time capabilities, and serverless functions.
## Serverless functions:
Supabase functions, also known as Edge Functions, are deployed at the edge, ensuring fast execution times with built-in observability. These functions are written in TypeScript and are designed to work closely with your PostgreSQL database, allowing you to execute SQL queries directly from your functions. Edge Functions run server-side logic geographically close to users, offering low latency and great performance.
@@ -96,11 +97,11 @@ Appwrite Functions support multiple languages, including Node.js, Python, Ruby,
# Conclusion
Choosing the right serverless function provider depends on your specific needs and project requirements.
Choosing the right serverless function provider depends on your specific needs and project requirements.
Supabase offers excellent SQL integration and low latency with its edge functions, making it ideal for applications that require real-time data handling and minimal delays.
Supabase offers excellent SQL integration and low latency with its edge functions, making it ideal for applications that require real-time data handling and minimal delays.
Firebase stands out with its mature ecosystem and seamless integration with Google services, perfect for developers looking for a comprehensive and reliable platform.
Firebase stands out with its mature ecosystem and seamless integration with Google services, perfect for developers looking for a comprehensive and reliable platform.
Appwrite's flexibility in language support and modularity makes it a great choice for developers seeking a highly customizable backend solution.

View File

@@ -7,6 +7,7 @@ cover: /images/blog/best-pagination-technique/cover.png
timeToRead: 8
author: matej-baco
category: tutorial
callToAction: true
---
The Database is one of the cornerstones of every application. It's where you store everything your app needs to remember, compute later, or display to other users online. It's all smooth sailing until your database grows and your application starts lagging because it's trying to fetch and render 1,000 posts simultaneously. As a smart engineer, you quickly patch this with a `Show more` button. However, a few weeks later, you encounter a `Timeout` error. Turning to Stack Overflow, you find that copying and pasting solutions is no longer helping. With no other options, you start debugging and discover that the database returns over 50,000 posts each time a user opens your app. What do you do now?
@@ -196,7 +197,7 @@ const config = JSON.parse(open("config.json"));
export default function () {
const offset = Query.offset(__ENV.OFFSET);
const limit = 10;
http.get(`${config.endpoint}/databases/main/collections/posts/documents?queries[]=${offset}&queries[]=${limit}`, {
headers: {
'content-type': 'application/json',
@@ -264,7 +265,7 @@ const config = JSON.parse(open("config.json"));
export default function () {
const cursor = Query.cursorAfter(__ENV.CURSOR);
const limit = 10;
http.get(`${config.endpoint}/databases/main/collections/posts/documents?queries[]=${offset}&queries[]=${limit}`, {
headers: {
'content-type': 'application/json',

View File

@@ -8,6 +8,7 @@ timeToRead: 3
author: dennis-ivy
category: tutorial
featured: false
callToAction: true
---
I want to address an issue I've seen popping up on Stack Overflow and the Appwrite Discord server and address some of the reasons you may be getting this error, then walk you through some of the steps you can take to try and resolve it as well.
@@ -18,23 +19,23 @@ The error message you'll see in your console when trying to make a request to an
Before we start debugging this, let's talk about what a CORS error is.
Without diving deep into the topic, CORS (Cross-Origin Resource Sharing ) is a mechanism that allows a server to specify which origins can access it. By origins, I mean URLs.
Without diving deep into the topic, CORS (Cross-Origin Resource Sharing ) is a mechanism that allows a server to specify which origins can access it. By origins, I mean URLs.
Site A, our server sitting at `myapi.com` won't allow request coming from site B, which is our client app sitting at `myfrontend.com`.
This happens because our server has not added site B, `myfrontend.com`, to its list of allowed origins.
Any request coming from a URL that is not listed in our server's allowed origins will be rejected by our CORS policy.
This happens because our server has not added site B, `myfrontend.com`, to its list of allowed origins.
Any request coming from a URL that is not listed in our server's allowed origins will be rejected by our CORS policy.
The solution in this case would be to simply add `myfrontend.com` to the list of allowed origins.
![CORS error appwrite](/images/blog/cors-error/cors_example.png)
CORS is crucial because it provides a secure way to make requests across different origins.
CORS is crucial because it provides a secure way to make requests across different origins.
Without CORS, any website would be able to make requests to our server, and this would lead to major problems.
Imagine a malicious third party making a website `myfr0nt3nd.com` that key logs your user name and password, before making requests to your backend to validate the combination.
# Why you are getting a CORS error
Now let's try to figure this all out in the context of Appwrite and why you may be getting this error.
Now let's try to figure this all out in the context of Appwrite and why you may be getting this error.
I have listed three main reasons. If more arise, I will update the article to include them.
1. Origin not set in Console
@@ -43,7 +44,7 @@ I have listed three main reasons. If more arise, I will update the article to in
## 1 - Origin not set in Console
First, you'll want to check your Appwrite Console to make sure you have added a hostname and are making a request from the correct hostname.
First, you'll want to check your Appwrite Console to make sure you have added a hostname and are making a request from the correct hostname.
Make sure you have added a platform in your Appwrite console by going to the **Overview** tab, select your platform or adding one if you have none, and then ensure you have added a hostname.
A hostname is simply the domain you will be making the request from. In development, this will most likely be `localhost`. No need to add a port number or protocol here.
@@ -60,16 +61,16 @@ So if you find this is why you were getting a CORS error, you have a few ways of
## 3 - Incorrect ID on request
This one happens because of an improperly configured request, such as a typo when specifying a project ID. For example, when using the `listDocuments` method,
This one happens because of an improperly configured request, such as a typo when specifying a project ID. For example, when using the `listDocuments` method,
if the project ID is set incorrectly when the client is initialized, you will receive a CORS error.
Without diving into the details about how CORS works, the problem occurs when the browser tries to check if the origin is allowed.
Without diving into the details about how CORS works, the problem occurs when the browser tries to check if the origin is allowed.
The request returns a `40X` response, so the entire CORS check fails.
## Other things to consider
In most cases, the issues people face have to do with one of the above reasons listed and can be solved with the given suggestions.
However, if you are still running into issues, Ill keep an ongoing list of other possibilities and things to check for.
In most cases, the issues people face have to do with one of the above reasons listed and can be solved with the given suggestions.
However, if you are still running into issues, Ill keep an ongoing list of other possibilities and things to check for.
- Disabled CORS in browser. (Ive seen people have this issue with browser extensions)

View File

@@ -6,7 +6,7 @@ date: 2023-12-02
cover: /images/blog/kcollect.png
timeToRead: 5
author: aditya-oberai
category: case-studies
category: customer-stories
---
In 2019, Ryan OConnor was a mere university student when he started exploring the world of Korean popular music, or K-pop. One of the areas within the K-pop fan ecosystem that caught his interest was the concept of photo cards. For those new to the K-pop community, K-pop photo cards are artist-specific collectible cards that are possessed and traded similarly to sports or Pokemon trading cards. These photo cards are produced and distributed by K-pop record labels and have developed a substantial interest and following over the last few years.

View File

@@ -6,7 +6,7 @@ date: 2024-05-23
cover: /images/blog/case-study-langx/cover.png
timeToRead: 5
author: aditya-oberai
category: case-studies
category: customer-stories
---
Born in Istanbul, Turkey, Xue never needed to prioritize learning English as a language until he pursued further education at Boğaziçi University, where his Mechanical Engineering coursework was delivered in English. After graduating and working as an IT manager at a multinational import-export company, Xue moved to Canada and founded his own tech consulting firm. By now, he had started exploring various language exchange platforms such as Tandem as a learner; however, he soon realized there was no perfect platform for his needs.

View File

@@ -6,7 +6,7 @@ date: 2024-01-19
cover: /images/blog/majik-kids.png
timeToRead: 7
author: aditya-oberai
category: case-studies
category: customer-stories
---
# Ideating an alternative content platform for children
@@ -73,4 +73,4 @@ So far, they have
The team at Majik Kids appreciates how Appwrite Cloud **simplified their development process** and **accelerated productivity** by offering a scalable and reliable managed Backend-as-a-Service solution with significant cost savings.
Learn more about Majik Kids by visiting their [website](https://majikkids.com/).
Learn more about Majik Kids by visiting their [website](https://majikkids.com/).

View File

@@ -6,7 +6,7 @@ date: 2024-03-04
cover: /images/blog/case-study-myshoefitter.png
timeToRead: 5
author: aditya-oberai
category: case-studies
category: customer-stories
---
> “Appwrite has been a tremendous asset in implementing our IT infrastructure. Not only is the software an absolute game-changer, but the team is always there when you need them. The integrated user authentication and the ease of creating data structures have undoubtedly saved us several weeks' worth of time. For me, Appwrite is the perfect backend solution. All you have to do is sign up, and your backend is ready to go. I have never seen such an innovative and easy-to-understand backend solution before!” \

View File

@@ -6,7 +6,7 @@ date: 2024-04-12
cover: /images/blog/case-study-open-mind/cover.png
timeToRead: 5
author: aditya-oberai
category: case-studies
category: customer-stories
---
While still at school, David Forster noticed a substantial increase in the usage of narcotic substances by his peers. He saw that the consumption of narcotic substances led to a decline in the mental and physical health of these folks. However, at that time, the only educational forums on this topic that were accessible to people were Wiki pages with information that was too complex to understand. A lack of simple educational tools prevented David from helping his peers break out of a substance habit.
@@ -47,4 +47,4 @@ Although localized to regions within Europe, the Android app has amassed:
In Testflight, the Apple app amassed over 1000 beta users; however, changes in the App Store policies prevented them from further updating and promoting the Apple app. In recent times, however, the team has taken a new direction and is converting the Open Mind mobile app into a web app using Flutter Web. The current version of the web application is available at [openmindapp.de](https://openmindapp.de/) and is quickly gaining traction and users.
As the Open Mind application grows, we wish David and the App Innovators team the best of luck and look forward to their future ventures. You can learn more about them by visiting their [website](https://app-innovators.de/).
As the Open Mind application grows, we wish David and the App Innovators team the best of luck and look forward to their future ventures. You can learn more about them by visiting their [website](https://app-innovators.de/).

View File

@@ -6,7 +6,7 @@ date: 2023-12-02
cover: /images/blog/smartbee.png
timeToRead: 5
author: aditya-oberai
category: case-studies
category: customer-stories
---
In 2020, Sergio Ponguta and his brother started Smartbee, a company offering security and communications solutions for coal mining operations in Colombia. Both brothers, being formally educated in systems engineering, combined with a lack of fear of traversing down mines, felt comfortable launching this venture.
@@ -68,4 +68,4 @@ In Sergios own words,
> Just go for it, dont think twice. Try Appwrite, and you will love it!
Learn more about Smartbee by visiting their [website](https://smartbee.com.co/).
Learn more about Smartbee by visiting their [website](https://smartbee.com.co/).

View File

@@ -6,7 +6,7 @@ date: 2024-07-09
cover: /images/blog/case-study-undo/cover.png
timeToRead: 6
author: aditya-oberai
category: case-studies
category: customer-stories
---
Over the past decade, Jonas Janssen has seen the circular economy grow in Belgium, resulting in sustainable business models that focus on rental, resale, and recycling for consumer companies. At his previous job as a CTO, he interacted with several customers, often sustainability-focused small and medium-sized businesses (SMBs). However, many of these companies mentioned the lack of software solutions for managing logistics and supply chains in circular businesses. Major solutions providers like Microsoft and SAP would build software with extensive feature sets and large prices that these companies neither needed nor could afford. Simply put, there was no software solution in the market for circular businesses with a low barrier of entry and reasonable pricing.
@@ -64,4 +64,4 @@ Appwrite enabled UNDŌ to go from idea to first customer rapidly. In Jonass w
> Thanks to Appwrite and advances in technology, we were able to get an MVP out in 2/3 months with 1 developer.
We appreciate how UNDŌ is leveraging Appwrite to support businesses with eco-friendly, sustainable practices. We definitely look forward to their future endeavors. You can learn more about them by visiting their [website](https://undo.software/).
We appreciate how UNDŌ is leveraging Appwrite to support businesses with eco-friendly, sustainable practices. We definitely look forward to their future endeavors. You can learn more about them by visiting their [website](https://undo.software/).

View File

@@ -7,6 +7,7 @@ cover: /images/blog/defying-the-laws-of-web-animations/cover.png
timeToRead: 10
author: thomas-g-lopes
category: website
callToAction: true
---
If you're a frontend developer, you know that one of the scariest tasks you can receive is coding a complex web animation. If you're not a frontend developer, I bet that sounds even harder.
@@ -173,8 +174,6 @@ The video above showcases both Motion and Svelte transitions in action. The tabl
{/if}
```
{% call_to_action /%}
## Transitioning between sections
There's one other nifty feature of Motion that I didn't mention: It can seamlessly interrupt ongoing animations.

View File

@@ -0,0 +1,327 @@
---
layout: post
title: "Deno 2 vs Bun: which JavaScript runtime is right for you?"
description: Deno vs Bun, how they fit in the Node.js ecosystem, and when you might want to use one over the other.
date: 2024-11-22
cover: /images/blog/deno-vs-bun-javascript-runtime/cover.png
timeToRead: 10
author: ebenezer-don
category: tutorial
---
JavaScript runtimes are evolving beyond Node.js, and this gives developers access to new tools designed for modern workflows, performance, and security. Two of the most talked-about options today are **Deno 2** and **Bun**.
Both aim to improve the developer experience in different ways, and they each bring unique strengths and trade-offs.
In this article, we'll take a practical look at what they offer, how they differ, and when you might want to use one over the other.
## Jump ahead:
- [Origins and background](#origins-and-background)
- [Performance comparison](#performance-comparison)
- [Language support and TypeScript](#language-support-and-typescript)
- [Package management approaches](#package-management-approaches)
- [Security features and models](#security-features-and-models)
- [Development tooling](#development-tooling)
- [Node.js compatibility layer](#nodejs-compatibility-layer)
- [Developer experience and workflow](#developer-experience-and-workflow)
- [Resource management](#resource-management)
- [Ideal use cases](#ideal-use-cases)
- [Community and ecosystem overview](#community-and-ecosystem-overview)
# Origins and background {% #origins-and-background %}
Deno was created by Ryan Dahl, who also developed Node.js. After years of Node.js dominating the JavaScript ecosystem, Dahl became vocal about its shortcomings around **security**, **dependency management**, and the lack of **TypeScript support**.
Deno was designed to address these issues.
Bun was created by Jarred Sumner, and it takes a different approach. Bun focuses on **performance and ease of use**. The runtime is built with speed as its primary goal, targeting developers who want a faster, smoother experience.
# Performance comparison {% #performance-comparison %}
## What makes Bun so fast? {% #what-makes-bun-so-fast %}
Bun is widely recognized for its speed. It's written in Zig, a low-level programming language designed for performance, and uses **JavaScriptCore**, the engine behind Safari.
This focus on efficiency is apparent in nearly every aspect of Bun's runtime:
- **Cold start times:** Bun starts faster than both Deno and Node.js, making it ideal for tasks that need quick initialization, like CLI tools.
- **HTTP servers:** Benchmarks show Bun serving HTTP requests with lower latency and higher throughput compared to Deno or Node.js.
- **Package installation:** Bun's built-in package manager is faster than npm or even pnpm, making dependency setup almost instant.
These performance gains come from Bun's tightly integrated ecosystem.
By designing its runtime, package manager, and bundler to work together, Bun eliminates much of the overhead seen in traditional setups.
## How Deno balances performance {% #how-deno-balances-performance %}
Deno isn't designed to compete with Bun in raw speed, but that doesn't mean it's slow.
Deno is built with Rust and uses **V8**, the same JavaScript engine as Node.js and Chrome. The runtime focuses on predictable, reliable performance. It's well-suited for:
- **Long-running processes:** Deno's resource management ensures consistent memory usage, making it ideal for servers or APIs that run for extended periods.
- **Real-world workloads:** While Deno may not win in benchmarks against Bun, Deno performs well under typical conditions like handling API requests or running TypeScript-heavy applications.
The difference between Bun's and Deno's performance matters most in edge cases.
If you're building tools or systems where startup speed is critical, Bun will feel faster. For more complex, long-term projects, Deno's balanced approach might be preferable.
# Language support and TypeScript {% #language-support-and-typescript %}
## Deno's native TypeScript support {% #deno-native-typescript-support %}
One of Deno's advantages is its native TypeScript support. Unlike Node.js or Bun, you can run .ts files directly in Deno without requiring separate tools like Babel or TypeScript CLI.
The runtime compiles TypeScript files into JavaScript during the first execution and caches the results, so developers can use TypeScript out of the box.
This integration:
- Simplifies workflows by removing the need for external build tools for most use cases.
- Treats TypeScript as a first-class citizen, deeply integrated into the runtime.
- Supports excellent error reporting, with optional full type-checking using the `--check` flag.
If you're heavily invested in TypeScript, Deno's built-in support will significantly reduce setup complexity.
This makes it an excellent choice for TypeScript-first projects.
## Bun's approach to TypeScript {% #bun-typescript-approach %}
Bun also supports TypeScript but takes a different approach to integration. Like Deno, Bun transpiles TypeScript to JavaScript using its own optimized implementation based on its JavaScript engine.
However, while Deno integrates TypeScript deeply into its runtime and offers features like type-checking with the `--check` flag, Bun focuses solely on fast transpilation and does not perform type-checking natively. For strict type-checking and debugging, external tools like the TypeScript compiler (`tsc`) are required.
For simpler TypeScript projects, Bun's lightweight approach works well. But for larger or more complex applications that demand deeper TypeScript integration, Deno provides a more comprehensive developer experience.
# Package management approaches {% #package-management-approaches %}
## How Deno manages dependencies {% #deno-dependency-management %}
Deno's approach to dependencies is one of its boldest departures from Node.js. Instead of relying on a centralized package manager like npm, Deno uses **URL-based imports**.
For example:
```jsx
import { serve } from '<https://deno.land/std@0.192.0/http/server.ts>'
```
This method:
- Removes the need for a `node_modules` directory.
- Ensures every dependency is explicitly defined, reducing the chance of unexpected changes in the supply chain.
In addition to URL-based imports, Deno now supports npm compatibility, enabling developers to use Node.js packages directly.
This feature is still relatively new, and while it's highly functional, some edge cases may remain. Bun, by comparison, was built with npm integration as a core feature and might provide a more polished experience for npm users at this time.
## Bun's npm-first model {% #bun-npm-first-model %}
Bun fully embraces the npm ecosystem. Its built-in package manager is designed to be faster than npm or pnpm, and it supports nearly all npm packages out of the box.
This makes Bun an excellent choice for projects that rely heavily on existing Node.js libraries or for developers transitioning from Node.js.
One key strength of Bun is how quickly it installs and resolves dependencies. For example, running `bun install` on a project is significantly faster than running `npm install`, thanks to Bun's optimized dependency resolver and storage system.
This makes Bun a compelling option for developers seeking to streamline their workflows without sacrificing access to the rich npm ecosystem.
## Trade-offs {% #trade-offs %}
- **Deno:** Its modular, web-like approach is forward-thinking and secure, but it requires developers to adapt. The npm integration is improving, but it's not yet as polished as Bun's.
- **Bun:** Its tight npm integration and fast installs make Bun practical for Node.js projects. While focused on compatibility, it excels in performance and developer-friendly features like hot reloading and fast builds.
# Security features and models {% #security-features-and-models %}
## Deno's permissions model {% #deno-permissions-model %}
One of Deno's defining features is its **strict permissions model**. By default, Deno doesn't allow your code to access the file system, make network requests, or read environment variables unless you explicitly grant these permissions. For example:
```bash
deno run --allow-net --allow-read app.ts
```
This approach minimizes risks by ensuring that code can only interact with the system in ways that you, the developer, have approved. This is especially important in production environments or multi-tenant systems, where limiting what an application can do reduces the impact of potential vulnerabilities.
From a practical standpoint, the permissions model encourages developers to think carefully about the resources their code needs. However, if you're used to Node.js, you might find Deno's permissions model more restrictive at first.
## Bun's permissive approach {% #bun-permissive-approach %}
Bun takes a more traditional approach, similar to Node.js. By default, it doesn't restrict access to system resources, making it easier to get started but leaving security entirely in the hands of the developer. While this design choice aligns with Bun's philosophy of prioritizing speed and convenience, it can lead to unintended behaviors if you're not careful.
For example, a dependency or library you install might access files or make network requests without your knowledge. This isn't a dealbreaker for many developers, but it's a trade-off worth considering if security is a top priority.
## The trade-off {% #the-trade-off %}
The difference in philosophy here is significant:
- **Deno**: A secure-by-default model that requires more upfront configuration but provides peace of mind.
- **Bun**: A developer-friendly model that sacrifices some safety for ease of use and speed.
For production systems or applications handling sensitive data, Deno's model is preferable. For rapid prototyping or local development, Bun's permissiveness is less of a concern.
# Development tooling {% #development-tooling %}
## What Deno provides {% #deno-built-in-features %}
Deno aims to be a comprehensive toolkit for developers, offering several built-in features that eliminate the need for external dependencies. Some highlights include:
- **Test runner**: Deno includes a built-in test runner that supports synchronous and asynchronous tests, assertions, and isolation.
```javascript
Deno.test('example test', () => {
console.log('This is a test')
})
```
- **Bundler**: Previously included a simple module bundler, but this feature is now deprecated in favor of tools like esbuild.
- **Formatter and linter**: Deno's formatter and linter enforce consistent code style and best practices, with easy integration into CI/CD workflows.
- **Task runner (`deno task`)**: A lightweight alternative to npm scripts, allowing you to define and execute tasks in your project's `deno.json` file.
These tools are well-integrated and follow Deno's overall philosophy of minimalism and standards compliance. They may not always be as feature-rich as standalone alternatives, but for many use cases, they get the job done.
## What Bun provides {% #bun-built-in-features %}
Bun also provides built-in tools but focuses on performance and speed:
- **Test runner**: Like Deno, Bun includes a test runner, but it emphasizes faster execution for repetitive test workflows.
- **Bundler**: Bun's bundler is optimized for web development, supporting features like tree-shaking and minification for high-performance builds.
- **Package manager**: Bun's integrated package manager delivers exceptional installation speeds while maintaining strong npm compatibility.
Bun's tools excel in speed, making it an attractive option for tasks like testing, bundling, and dependency installation. However, unlike Deno, Bun prioritizes performance and practicality over strict adherence to web standards, which may be a trade-off for some developers.
## Developer workflow comparison {% #developer-workflow-comparison %}
- **Deno**: Offers a cohesive, standards-compliant toolkit that integrates well with modern development practices but may require additional configuration for advanced use cases.
- **Bun**: Prioritizes speed and convenience, making it ideal for iterative development and performance-critical tasks.
For day-to-day development, the choice depends on your priorities. Deno's tools emphasize stability and consistency, while Bun is designed to optimize speed and productivity.
# Node.js compatibility layer {% #nodejs-compatibility-layer %}
## Deno's growing compatibility {% #deno-nodejs-compatibility %}
Node.js compatibility has been one of Deno's biggest challenges. While Deno initially distanced itself from Node.js conventions (e.g., by avoiding the `require` syntax), it has gradually introduced features to support Node.js projects. Recent versions of Deno include:
- **npm support**: Deno can now use npm packages, which bridges the gap for developers relying on Node.js libraries.
- **Node.js API support**: Deno has implemented key Node.js APIs like `fs` and `events`, although not all features are fully supported yet.
Despite these advancements, there are still edge cases where Node.js compatibility isn't perfect. Libraries that rely heavily on native Node.js modules or older patterns may require additional effort to work in Deno.
## Bun as a drop-in replacement {% #bun-nodejs-compatibility %}
Bun, by contrast, aims to be a near-drop-in replacement for Node.js. Its compatibility with npm is excellent, and it supports almost all Node.js APIs out of the box. This makes it an attractive option for developers looking to migrate existing projects without rewriting significant portions of code.
However, even Bun isn't flawless. Certain edge cases, especially with less-used or experimental Node.js features, might still cause issues. But for the majority of projects, Bun's compatibility is seamless.
## Migrating existing projects {% #migrating-existing-projects %}
If you're transitioning from Node.js:
- **Bun** offers the path of least resistance, especially for npm-heavy projects.
- **Deno** requires more adaptation, especially for projects that rely on older Node.js conventions. However, its npm integration is improving rapidly and may soon close the gap.
# Developer experience and workflow {% #developer-experience-and-workflow %}
## Ease of use {% #ease-of-use %}
Both Deno and Bun strive to improve the developer experience, but their approaches differ:
- **Deno**: Focuses on clarity and standards. Its permission model and URL-based imports enforce best practices, but they can feel restrictive if you're coming from Node.js.
- **Bun**: Prioritizes familiarity and speed. It feels more like a performance-enhanced version of Node.js, which makes it easier to pick up for most developers.
## Documentation and resources {% #documentation-and-resources %}
- **Deno**: Has extensive official documentation, a growing ecosystem of tutorials, and strong community support. Its focus on standards also makes it easier to learn for developers already familiar with browser-based JavaScript.
- **Bun**: While newer and with a smaller community, Bun has clear documentation and is gaining traction quickly. Its npm compatibility makes it easy to leverage existing Node.js resources.
## Ecosystem maturity {% #ecosystem-maturity %}
Deno has been around longer and has a more mature ecosystem, with good TypeScript support and a steadily growing collection of modules on `deno.land`. Bun, while newer, is gaining momentum quickly, especially among developers looking for a high-performance alternative to Node.js.
# Resource management {% #resource-management %}
## Deno's resource management {% #deno-resource-management %}
Deno's architecture emphasizes memory safety and predictable resource usage, thanks to its foundation in Rust and V8. Rust enforces strict memory management, which ensures that Deno applications avoid common issues like memory leaks or unsafe pointer usage. Additionally, Deno provides a transparent way to manage resources:
- **Garbage Collection:** Like Node.js, Deno relies on V8 for garbage collection, ensuring that unused objects are cleaned up efficiently.
- **Concurrency:** Deno's async programming model and event loop enable efficient handling of high-concurrency workloads, such as managing multiple API requests or real-time applications.
This predictable resource usage makes Deno a strong choice for long-running processes or cloud-based applications where memory efficiency directly impacts operational costs.
## Bun's resource optimization {% #bun-resource-optimization %}
Bun, built with Zig and JavaScriptCore, prioritizes raw speed and lightweight resource usage. Zig allows developers to write highly optimized, low-level code, giving Bun an edge in tasks that require minimal overhead. For example:
- **Startup memory usage:** Bun often uses less memory at startup compared to Deno or Node.js.
- **CPU usage:** Bun's tight integration with JavaScriptCore results in lower CPU usage for tasks like serving static files or running small scripts.
However, Bun's strong focus on speed sometimes comes at the cost of detailed visibility into resource consumption. For production environments, you might need to monitor closely to avoid potential inefficiencies in complex use cases.
## When resource usage matters {% #when-resource-usage-matters %}
- If your application needs to run on constrained environments (e.g., serverless functions or edge devices), both Deno and Bun offer advantages over Node.js, but their strengths differ:
- **Deno** provides consistent and predictable resource management, making it well-suited for long-running processes or workloads where stability is critical.
- **Bun** performs better in lightweight or ephemeral tasks but may require more careful monitoring for resource-heavy workloads.
# Ideal use cases {% #ideal-use-cases %}
## When to choose Deno {% #when-to-choose-deno %}
Deno's design philosophy makes it ideal for scenarios where security, maintainability, and standards compliance are priorities:
- **TypeScript-first projects:** With native TypeScript support, Deno removes the friction of configuring a separate build pipeline.
- **Secure applications:** Deno's permissions model is a clear win for projects handling sensitive data or operating in multi-tenant environments.
- **Modern API development:** Its web-like module system and built-in tooling make it well-suited for REST or GraphQL APIs.
- **CLI tools:** Deno's simplicity and predictability make it a strong contender for command-line utilities, especially those using TypeScript.
## When to choose Bun {% #when-to-choose-bun %}
Bun's strengths lie in its speed and npm compatibility, making it a better fit for projects where performance and integration with existing tools are key:
- **Performance-critical applications:** If startup time or execution speed is a top priority, Bun is the faster option.
- **Node.js migrations:** For developers transitioning from Node.js, Bun offers seamless npm compatibility with little adjustment needed.
- **Prototyping and development:** Its speed and built-in tools help iterate quickly on small to medium-sized projects.
- **Web applications:** Bun's bundler and npm integration make it appealing for modern web development workflows.
## Hybrid use cases {% #hybrid-use-cases %}
There are situations where using both runtimes in a single project might make sense. For instance:
- Use **Deno** for a secure backend API and **Bun** for a fast, lightweight front-end build tool.
- Use **Bun** for rapid prototyping and continue with it or transition to **Deno** for production if security and standards compliance become higher priorities.
While these setups are possible, combining runtimes may add complexity to your development workflow.
# Community and ecosystem overview {% #community-and-ecosystem-overview %}
## Deno's ecosystem {% #deno-ecosystem %}
Deno has been around longer and benefits from a growing, mature ecosystem. Its official module registry, `deno.land`, includes a curated collection of high-quality libraries and tools, many of which are standards-based. Additionally, its compatibility with npm packages is improving, broadening the range of tools available to developers.
The Deno community is active, with strong contributions from developers and detailed documentation that makes onboarding relatively smooth. However, it still faces challenges in convincing Node.js developers to adapt to its unique features, like URL-based imports and strict permissions.
## Bun's ecosystem {% #bun-ecosystem %}
Bun's ecosystem is smaller but growing rapidly. Its focus on npm compatibility ensures that developers have access to the large Node.js library ecosystem from day one. This makes Bun feel less like a standalone runtime and more like a high-performance layer on top of Node.js.
The Bun community, while smaller, is enthusiastic and growing fast. Documentation is clear and concise, but as a newer tool, it may lack the breadth of resources that Deno offers.
## Maturity vs. momentum {% #maturity-vs-momentum %}
- **Deno** benefits from a more mature and standards-driven ecosystem.
- **Bun** capitalizes on its compatibility with npm and Node.js, giving it momentum as a fast alternative.
## Deno 2 vs. Bun comparison table {% #deno-2-vs-bun-comparison-table %}
| **Feature** | **Deno** | **Bun** |
| ---------------------- | ------------------------------------------------- | ------------------------------------------------ |
| **Philosophy** | Secure-by-default, standards-compliant | Performance-focused, npm-centric |
| **Primary Language** | Rust, V8 JavaScript engine | Zig, JavaScriptCore engine |
| **TypeScript Support** | Native support; no configuration needed | Transpiles TypeScript; external type-checking |
| **Module System** | URL-based imports; supports npm packages | Integrated with npm; uses `node_modules` |
| **Performance** | Balanced performance in various tasks | Optimized for speed, especially in server tasks |
| **Security** | Explicit permissions model | No default restrictions |
| **Node.js Compatibility** | Growing support with npm integration | High compatibility; near drop-in replacement |
| **Tooling and Packages** | Runtime, bundler, test runner, formatter, linter | Runtime, bundler, test runner, package manager |
| **Community and Maturity** | Established, standards-driven | Emerging, rapidly growing |
# Conclusion {% #conclusion %}
Deno 2 and Bun are both impressive JavaScript runtimes, but they serve different purposes and cater to different priorities. Ultimately, the right runtime depends on your use case. If you're building a secure backend API or a TypeScript-heavy CLI tool, **Deno 2** is hard to beat. If you're looking for raw speed or want to stay close to the Node.js ecosystem, **Bun** is an excellent option.
JavaScript is evolving, and both Deno 2 and Bun reflect the direction it's heading. While they take different paths, they both push the boundaries of what developers can expect from modern runtimes.
If you enjoyed this article, you might also enjoy [Building apps with Bun and Appwrite](https://appwrite.io/blog/post/building-apps-with-bun-and-appwrite) or reading about [what Deno 2 means for Appwrite Functions](https://appwrite.io/blog/post/deno-2-appwrite-functions).
You can reach out to us on [Discord](https://appwrite.io/discord) if you have any questions or send me a message on [LinkedIn](https://www.linkedin.com/in/ebenezerdon/) if you have any feedback.
# More resources {% #more-resources %}
- [Join Appwrite's Discord server](https://appwrite.io/discord)
- [Build a currency converter with Deno 2 and Appwrite](https://www.appwrite.io/blog/post/build-a-currency-converter-with-deno2)
- [Why you need to try the new Bun function runtime](https://appwrite.io/blog/post/why-you-need-to-try-the-new-bun-runtime)

Some files were not shown because too many files have changed in this diff Show More