{"id":29556838,"url":"https://github.com/ntalbotdev/answerly-nuxt","last_synced_at":"2026-04-12T06:32:24.049Z","repository":{"id":303735373,"uuid":"1016374096","full_name":"ntalbotdev/answerly-nuxt","owner":"ntalbotdev","description":"Answerly - Q\u0026A social app (Nuxt + Supabase)","archived":false,"fork":false,"pushed_at":"2025-07-16T08:05:51.000Z","size":545,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-07-17T02:07:55.641Z","etag":null,"topics":["nuxt","pinia","postgresql","scss","supabase","tailwindcss","typescript","vercel"],"latest_commit_sha":null,"homepage":"https://answerly-nuxt.vercel.app","language":"Vue","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ntalbotdev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-07-08T23:23:05.000Z","updated_at":"2025-07-16T08:05:50.000Z","dependencies_parsed_at":"2025-07-09T06:51:30.812Z","dependency_job_id":null,"html_url":"https://github.com/ntalbotdev/answerly-nuxt","commit_stats":null,"previous_names":["ntalbotdev/answerly-nuxt"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ntalbotdev/answerly-nuxt","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ntalbotdev%2Fanswerly-nuxt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ntalbotdev%2Fanswerly-nuxt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ntalbotdev%2Fanswerly-nuxt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ntalbotdev%2Fanswerly-nuxt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ntalbotdev","download_url":"https://codeload.github.com/ntalbotdev/answerly-nuxt/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ntalbotdev%2Fanswerly-nuxt/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265746268,"owners_count":23821634,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nuxt","pinia","postgresql","scss","supabase","tailwindcss","typescript","vercel"],"created_at":"2025-07-18T11:00:51.517Z","updated_at":"2026-04-12T06:32:24.041Z","avatar_url":"https://github.com/ntalbotdev.png","language":"Vue","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Answerly Nuxt App\n\nA robust Nuxt 4 CRUD application leveraging Supabase for authentication, database management, and profile asset storage, Pinia for state handling, and Tailwind CSS for modern UI styling. Testing is provided via Vitest for unit tests and Playwright for end-to-end automation.\n\n## Features\n\n- Supabase Auth (email/password)\n  - Email verification on signup\n  - Password reset functionality\n- User profile creation and management\n- Public profile pages\n- Profile editing and assets upload (avatar/banner via Supabase Storage)\n- Ask questions to any user (optionally anonymously)\n  - Anonymous questions protect user privacy and show as \"Anonymous\" in notifications\n- Users can answer questions they receive\n- Questions are only published after being answered\n- Follow/unfollow users (social feature)\n- View followers and following lists for any user\n- See mutual follow status on profiles\n- Real-time notification system with automatic cleanup\n  - Get notified when someone follows you\n  - Get notified when someone asks you a question\n  - Get notified when someone answers your question\n  - Notifications are automatically removed when actions are completed (questions answered/deleted, users unfollowed)\n  - Real-time updates using Supabase subscriptions\n  - Mark notifications as read (delete from system)\n  - Clear all notifications at once\n- Real-time inbox system\n  - Questions appear instantly when received\n  - Real-time updates when questions are answered or deleted\n- Pinia for state management\n- Middleware for route protection and redirects\n- See the full [TODO list](./TODO.md) for upcoming features and improvements\n\n## Project Structure\n\n- `app/` — Main application source folder (Nuxt 4 standard)\n  - `assets/` — Static assets\n  - `components/` — Reusable Vue components\n  - `composables/` — Reusable composable functions\n  - `layouts/` — Nuxt layouts\n  - `middleware/` — Route guards and redirects\n  - `pages/` — Nuxt pages (routes)\n    - `profile/[username]/` — Profile pages using shared components\n  - `stores/` — Pinia stores (profile, questions, notifications)\n  - `utils/` — Utility functions and constants\n- `scripts/` — Scripts for automation and development tasks\n- `tests/` — Unit and end-to-end tests\n  - `e2e/` — End-to-end tests using Playwright\n  - `unit/` — Unit tests using Vitest\n\n## Environment Setup\n\n1. **Environment Variables**\n    - Create a `.env` file in the project root with:\n        ```\n        SUPABASE_URL=your-supabase-url\n        SUPABASE_ANON_KEY=your-supabase-anon-key\n        ```\n2. **Nuxt Modules**\n    - `@nuxtjs/supabase`\n    - `@pinia/nuxt`\n    - `@nuxtjs/tailwindcss`\n    - `@nuxt/test-utils/module`\n    - `@nuxt/eslint`\n    - `@nuxt/icon`\n\n3. **Install dependencies**\n    ```\n    npm install\n    ```\n\n## Supabase Setup\n\n### Enable Auth \u0026 Database\n\n- Go to [Supabase](https://app.supabase.com/)\n- Create a new project (Supabase Database/Postgres)\n- Enable email/password authentication in the Auth settings\n\n### Database Schema\n\n#### `profiles` Table\n\n| Column       | Type        | Description                      |\n| ------------ | ----------- | -------------------------------- |\n| user_id      | uuid        | Primary key, Default: auth.uid() |\n| username     | text        | Unique, required                 |\n| display_name | text        | Nullable, user display name      |\n| avatar_url   | text        | Nullable, profile picture URL    |\n| banner_url   | text        | Nullable, profile banner URL     |\n| bio          | text        | Nullable, user bio               |\n| created_at   | timestamptz | Default: now()                   |\n| updated_at   | timestamptz | Default: now()                   |\n\n\u003cdetails\u003e\n  \u003csummary\u003e📄 \u003cstrong\u003eProfiles Table SQL Query\u003c/strong\u003e\u003c/summary\u003e\n\n  ```sql\n  create table profiles (\n    user_id uuid primary key default auth.uid() on delete cascade,\n    username text unique not null,\n    display_name text,\n    avatar_url text,\n    banner_url text,\n    bio text,\n    created_at timestamptz not null default now(),\n    updated_at timestamptz not null default now()\n  );\n  ```\n\u003c/details\u003e\n\n#### `follows` Table\n\n| Column       | Type        | Description                               |\n| ------------ | ----------- | ----------------------------------------- |\n| follower_id  | uuid        | Primary key, references profiles(user_id) |\n| following_id | uuid        | Primary key, references profiles(user_id) |\n| created_at   | timestamptz | Default: now()                            |\n\n\u003cdetails\u003e\n  \u003csummary\u003e📄 \u003cstrong\u003eFollows Table SQL Query\u003c/strong\u003e\u003c/summary\u003e\n\n  ```sql\n  create table follows (\n    follower_id uuid references profiles(user_id) on delete cascade,\n    following_id uuid references profiles(user_id) on delete cascade,\n    created_at timestamptz not null default now(),\n    primary key (follower_id, following_id)\n  );\n  ```\n\u003c/details\u003e\n\n#### `questions` Table\n\n| Column       | Type        | Description         |\n| ------------ | ----------- | ------------------- |\n| id           | uuid        | Primary key         |\n| from_user_id | uuid        | From user, required |\n| to_user_id   | uuid        | To user, required   |\n| question     | text        | Required            |\n| is_anonymous | boolean     | Default: false      |\n| answer       | text        | Nullable            |\n| published    | boolean     | Default: false      |\n| created_at   | timestamptz | Default: now()      |\n| answered_at  | timestamptz | Nullable            |\n\n\u003cdetails\u003e\n  \u003csummary\u003e📄 \u003cstrong\u003eQuestions Table SQL Query\u003c/strong\u003e\u003c/summary\u003e\n\n  ```sql\n  create table questions (\n    id uuid primary key default gen_random_uuid(),\n    from_user_id uuid not null references profiles(user_id) on delete cascade,\n    to_user_id uuid not null references profiles(user_id) on delete cascade,\n    question text not null,\n    is_anonymous boolean not null default false,\n    answer text,\n    published boolean not null default false,\n    created_at timestamptz not null default now(),\n    answered_at timestamptz\n  );\n  ```\n\u003c/details\u003e\n\n#### `notifications` Table\n\n| Column       | Type        | Description             |\n| ------------ | ----------- | ----------------------- |\n| id           | uuid        | Primary key             |\n| user_id      | uuid        | User ID, required       |\n| type         | text        | Notification type       |\n| payload      | jsonb       | Nullable, Flexible data |\n| created_at   | timestamptz | Default: now()          |\n| event_id     | text        | Generated from payload  |\n\n\u003cdetails\u003e\n  \u003csummary\u003e📄 \u003cstrong\u003eNotifications Table SQL Query\u003c/strong\u003e\u003c/summary\u003e\n\n  ```sql\n  create table notifications (\n    id uuid primary key default gen_random_uuid(),\n    user_id uuid not null references profiles(user_id) on delete cascade,\n    type text not null check (type in ('follow', 'question', 'answer', 'system')),\n    payload jsonb,\n    created_at timestamptz not null default now(),\n    event_id text generated always as (\n      case\n        when type = 'follow' then COALESCE(payload::jsonb-\u003e\u003e'follower_id', '') || ':' || COALESCE(payload::jsonb-\u003e\u003e'following_id', '')\n        when type = 'question' then COALESCE(payload::jsonb-\u003e\u003e'question_id', '')\n        when type = 'answer' then COALESCE(payload::jsonb-\u003e\u003e'question_id', '')\n        else COALESCE(id::text, '')\n      end\n    ) stored,\n    unique (user_id, type, event_id)\n  );\n  ```\n\u003c/details\u003e\n\n### Edge Functions\n\n\u003cdetails\u003e\n  \u003csummary\u003e🔧 \u003cstrong\u003esend-notification Edge Function\u003c/strong\u003e\u003c/summary\u003e\n\n  ```ts\n  import { serve } from \"https://deno.land/std@0.168.0/http/server.ts\";\n  import { createClient } from \"https://esm.sh/@supabase/supabase-js@2\";\n\n  serve(async (req) =\u003e {\n    const origin = req.headers.get(\"origin\") || \"*\";\n    if (req.method === \"OPTIONS\") {\n      return new Response(null, {\n        status: 204,\n        headers: {\n          \"Access-Control-Allow-Origin\": origin,\n          \"Access-Control-Allow-Methods\": \"POST, DELETE, OPTIONS\",\n          \"Access-Control-Allow-Headers\": \"Content-Type, Authorization\"\n        }\n      });\n    }\n    const supabaseUrl = Deno.env.get(\"SUPABASE_URL\");\n    const supabaseServiceRoleKey = Deno.env.get(\"SUPABASE_SERVICE_ROLE_KEY\");\n    if (!supabaseUrl || !supabaseServiceRoleKey) {\n      return new Response(\"Missing environment variables\", {\n        status: 500,\n        headers: {\n          \"Access-Control-Allow-Origin\": origin\n        }\n      });\n    }\n    const supabase = createClient(supabaseUrl, supabaseServiceRoleKey);\n    if (req.method === \"DELETE\") {\n      const { user_id, event_id, type } = await req.json();\n      if (!user_id || !event_id || !type) {\n        return new Response(\"Missing required fields for deletion\", {\n          status: 400,\n          headers: {\n            \"Access-Control-Allow-Origin\": origin\n          }\n        });\n      }\n      const { error } = await supabase.from(\"notifications\")\n        .delete()\n        .eq(\"user_id\", user_id)\n        .eq(\"event_id\", event_id)\n        .eq(\"type\", type);\n      if (error) {\n        return new Response(`Error deleting notification: ${error.message}`, {\n          status: 500,\n          headers: {\n            \"Access-Control-Allow-Origin\": origin\n          }\n        });\n      }\n      return new Response(\"Notification deleted\", {\n        status: 200,\n        headers: {\n          \"Access-Control-Allow-Origin\": origin\n        }\n      });\n    }\n    const { user_id, type, payload } = await req.json();\n    if (!user_id || !type) {\n      return new Response(\"Missing required fields\", {\n        status: 400,\n        headers: {\n          \"Access-Control-Allow-Origin\": origin\n        }\n      });\n    }\n    const { data, error } = await supabase.from(\"notifications\").upsert([\n      {\n        user_id,\n        type,\n        payload\n      }\n    ], {\n      onConflict: [\n        \"user_id\",\n        \"type\",\n        \"event_id\"\n      ]\n    }).select();\n    if (error) {\n      return new Response(`Error: ${error.message}`, {\n        status: 500,\n        headers: {\n          \"Access-Control-Allow-Origin\": origin\n        }\n      });\n    }\n    return new Response(\"Notification inserted\", {\n      status: 200,\n      headers: {\n        \"Access-Control-Allow-Origin\": origin\n      }\n    });\n  });\n  ```\n\u003c/details\u003e\n\n## Storage \u0026 Profile Assets\n\n- Each user can upload an avatar and a banner image to their own folder: `[user_id]/avatar.webp` and `[user_id]/banner.webp`\n- The `avatar_url` and `banner_url` fields in the profile point to the public URLs of the uploaded images\n- Make the bucket public for public URLs\n- Use the RLS policies below to restrict access to profile assets\n\n## Row Level Security (RLS)\n\n\u003e ℹ️ Click the spoilers below to reveal the SQL queries for each policy \n\n\u003cdetails\u003e\n  \u003csummary\u003e📦 \u003cstrong\u003eStorage RLS Policies\u003c/strong\u003e\u003c/summary\u003e\n    \n  ```sql\n  CREATE POLICY \"Public can view avatar/banner\"\n    ON storage.objects\n    FOR SELECT\n    TO public\n    USING (\n      bucket_id = 'profile-assets'\n      AND (\n        name LIKE '%/avatar.webp'\n        OR name LIKE '%/banner.webp'\n      )\n    );\n\n  CREATE POLICY \"Users can upload avatar/banner to their folder\"\n    ON storage.objects\n    FOR INSERT\n    TO authenticated\n    WITH CHECK (\n      bucket_id = 'profile-assets'\n      AND (\n        name = (select auth.uid())::text || '/avatar.webp'\n        OR name = (select auth.uid())::text || '/banner.webp'\n      )\n    );\n\n  CREATE POLICY \"Users can update avatar/banner in their folder\"\n    ON storage.objects\n    FOR UPDATE\n    TO authenticated\n    USING (\n      bucket_id = 'profile-assets'\n      AND (\n        name = (select auth.uid())::text || '/avatar.webp'\n        OR name = (select auth.uid())::text || '/banner.webp'\n      )\n    )\n    WITH CHECK (\n      bucket_id = 'profile-assets'\n      AND (\n        name = (select auth.uid())::text || '/avatar.webp'\n        OR name = (select auth.uid())::text || '/banner.webp'\n      )\n    );\n\n  CREATE POLICY \"Users can delete avatar/banner from their folder\"\n    ON storage.objects\n    FOR DELETE\n    TO authenticated\n    USING (\n      bucket_id = 'profile-assets'\n      AND (\n        name = (select auth.uid())::text || '/avatar.webp'\n        OR name = (select auth.uid())::text || '/banner.webp'\n      )\n    );\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e👤 \u003cstrong\u003eProfiles RLS Policies\u003c/strong\u003e\u003c/summary\u003e\n\n  ```sql\n  CREATE POLICY \"Public can view profiles\"\n    ON profiles\n    FOR SELECT\n    TO public\n    USING (true);\n\n  CREATE POLICY \"Users can create their own profile\"\n    ON profiles\n    FOR INSERT\n    TO authenticated\n    WITH CHECK (\n      user_id = (select auth.uid())\n    );\n\n  CREATE POLICY \"Users can update their own profile\"\n    ON profiles\n    FOR UPDATE\n    TO authenticated\n    USING (\n      user_id = (select auth.uid())\n    )\n    WITH CHECK (\n      user_id = (select auth.uid())\n    );\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🤝 \u003cstrong\u003eFollows RLS Policies\u003c/strong\u003e\u003c/summary\u003e\n\n  ```sql\n  CREATE POLICY \"Everyone can view follows\"\n    ON follows\n    FOR SELECT\n    TO public\n    USING (true);\n\n  CREATE POLICY \"Users can follow others\"\n    ON follows\n    FOR INSERT\n    TO authenticated\n    WITH CHECK (\n      follower_id = (select auth.uid())\n    );\n\n  CREATE POLICY \"Users can unfollow others\"\n    ON follows\n    FOR DELETE\n    TO authenticated\n    USING (\n      follower_id = (select auth.uid())\n    );\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e❓ \u003cstrong\u003eQuestions RLS Policies\u003c/strong\u003e\u003c/summary\u003e\n\n  ```sql\n  CREATE POLICY \"Public can view published questions\"\n    ON questions\n    FOR SELECT\n    TO public\n    USING (\n      published = true\n    );\n\n  CREATE POLICY \"Users can view their own questions\"\n    ON questions\n    FOR SELECT\n    TO authenticated\n    USING (\n      from_user_id = (select auth.uid())\n      OR to_user_id = (select auth.uid())\n    );\n\n  CREATE POLICY \"Users can ask questions\"\n    ON questions\n    FOR INSERT\n    TO authenticated\n    WITH CHECK (\n      from_user_id = (select auth.uid())\n    );\n\n  CREATE POLICY \"Users can answer questions sent to them\"\n    ON questions\n    FOR UPDATE\n    TO authenticated\n    USING (\n      to_user_id = (select auth.uid())\n    )\n    WITH CHECK (\n      to_user_id = (select auth.uid())\n    );\n\n  CREATE POLICY \"Users can delete questions they asked or received\"\n    ON questions\n    FOR DELETE\n    TO authenticated\n    USING (\n      from_user_id = (select auth.uid())\n      OR to_user_id = (select auth.uid())\n    );\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🔔 \u003cstrong\u003eNotifications RLS Policies\u003c/strong\u003e\u003c/summary\u003e\n\n  ```sql\n  CREATE POLICY \"Users can view their notifications\"\n    ON notifications\n    FOR SELECT\n    TO authenticated\n    USING (\n      user_id = (select auth.uid())\n    );\n\n  CREATE POLICY \"Users can update their notifications\"\n    ON notifications\n    FOR UPDATE\n    TO authenticated\n    USING (\n      user_id = (select auth.uid())\n    )\n    WITH CHECK (\n      user_id = (select auth.uid())\n    );\n\n  CREATE POLICY \"Users can delete their notifications\"\n    ON notifications\n    FOR DELETE\n    TO authenticated\n    USING (\n      user_id = (select auth.uid())\n    );\n\n  CREATE POLICY \"Allow system inserts for notifications\"\n    ON notifications\n    FOR INSERT\n    TO service_role\n    WITH CHECK (true);\n  ```\n\u003c/details\u003e\n\n## Usage\n\n- Sign up and log in with email/password (needs email verification)\n- After signup, a profile is created in the `profiles` table\n- Visit `/` as a guest to see the welcome page; as a logged-in user, you'll see your feed\n- Visit `/inbox` to answer questions sent to you (only published after answering)\n  - Real-time updates when new questions arrive\n  - Answering or deleting questions automatically removes related notifications\n- Visit `/notifications` to see real-time notifications for user activity and events\n  - Follow notifications: See who followed you\n  - Question notifications: See new questions you received\n  - Answer notifications: See when your questions are answered\n  - Click \"Mark as read\" to permanently delete individual notifications\n  - Use \"Clear all\" to remove all notifications at once\n- Visit `/my-questions` to see questions you have asked others\n- Visit `/profile/:username` to view a public profile (ex: [/profile/axile](https://answerly-nuxt.vercel.app/profile/axile))\n  - If it's your own profile, you can edit it by clicking the edit button\n  - Follow/unfollow users with automatic notification management\n- Visit `/profile/:username/followers` to see a user's followers\n- Visit `/profile/:username/following` to see who a user is following\n\n## Development\n\n```bash\nnpm run dev\n```\n\n## Testing\n\n- Run unit tests using Vitest:\n  ```bash\n  npm run test\n  ```\n\n- Run end-to-end tests using Playwright:\n  ```bash\n  npm run test:e2e\n  ```\n  - This will generate `tests/e2e/auth.json` with authenticated user session data and open Playwright in UI mode.\n\n- Use Playwright codegen to generate tests:\n  ```bash\n  npm run codegen:e2e\n  ```\n  - This will open a browser window where you can interact with the app and record your actions.\n\n### Test User\n- Email: `test@test.com`\n- Password: `test123`\n\n---\n\n**Built with Nuxt, Supabase, Pinia, and Tailwind CSS.**\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fntalbotdev%2Fanswerly-nuxt","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fntalbotdev%2Fanswerly-nuxt","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fntalbotdev%2Fanswerly-nuxt/lists"}